mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
complete rule engine and booking flow
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddBookingsRemainingForeignKeys1748800000000 implements MigrationInterface {
|
||||
name = 'AddBookingsRemainingForeignKeys1748800000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// ── freight.bookings: nullable FK cleanup ─────────────────────────────
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings b
|
||||
SET train_id = NULL
|
||||
WHERE b.train_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.trains t WHERE t.id = b.train_id);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings b
|
||||
SET previous_contract_id = NULL
|
||||
WHERE b.previous_contract_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.bookings pb WHERE pb.id = b.previous_contract_id);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings b
|
||||
SET consolidation_partner_id = NULL
|
||||
WHERE b.consolidation_partner_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.bookings pb WHERE pb.id = b.consolidation_partner_id);
|
||||
`);
|
||||
|
||||
// Remove bookings with no matching public.customers row
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_cargo_modifier bcm
|
||||
USING freight.bookings b
|
||||
WHERE bcm.booking_id = b.id
|
||||
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_approval_step bas
|
||||
USING freight.bookings b
|
||||
WHERE bas.booking_id = b.id
|
||||
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_rate_snapshot brs
|
||||
USING freight.bookings b
|
||||
WHERE brs.booking_id = b.id
|
||||
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_container bc
|
||||
USING freight.bookings b
|
||||
WHERE bc.booking_id = b.id
|
||||
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.bookings b
|
||||
WHERE NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
|
||||
`);
|
||||
|
||||
// ── freight.bookings FKs ────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.bookings
|
||||
ADD CONSTRAINT "FK_bookings_customer_id"
|
||||
FOREIGN KEY (customer_id)
|
||||
REFERENCES public.customers(id)
|
||||
ON DELETE RESTRICT;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.bookings
|
||||
ADD CONSTRAINT "FK_bookings_train_id"
|
||||
FOREIGN KEY (train_id)
|
||||
REFERENCES freight.trains(id)
|
||||
ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.bookings
|
||||
ADD CONSTRAINT "FK_bookings_previous_contract_id"
|
||||
FOREIGN KEY (previous_contract_id)
|
||||
REFERENCES freight.bookings(id)
|
||||
ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.bookings
|
||||
ADD CONSTRAINT "FK_bookings_consolidation_partner_id"
|
||||
FOREIGN KEY (consolidation_partner_id)
|
||||
REFERENCES freight.bookings(id)
|
||||
ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
// ── freight.booking_container ─────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.booking_container bc
|
||||
SET weight_limit_rule_id = NULL
|
||||
WHERE bc.weight_limit_rule_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM freight.weight_limit_rules wlr WHERE wlr.id = bc.weight_limit_rule_id
|
||||
);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_container bc
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.container_types ct WHERE ct.id = bc.container_type_id);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.booking_container
|
||||
ADD CONSTRAINT "FK_booking_container_container_type_id"
|
||||
FOREIGN KEY (container_type_id)
|
||||
REFERENCES freight.container_types(id)
|
||||
ON DELETE RESTRICT;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.booking_container
|
||||
ADD CONSTRAINT "FK_booking_container_weight_limit_rule_id"
|
||||
FOREIGN KEY (weight_limit_rule_id)
|
||||
REFERENCES freight.weight_limit_rules(id)
|
||||
ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
// ── freight.booking_rate_snapshot ─────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_cargo_modifier bcm
|
||||
USING freight.booking_rate_snapshot brs
|
||||
WHERE bcm.rate_snapshot_id = brs.id
|
||||
AND (
|
||||
NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = brs.booking_id)
|
||||
OR NOT EXISTS (SELECT 1 FROM freight.rates r WHERE r.id = brs.rate_id)
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_rate_snapshot brs
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = brs.booking_id)
|
||||
OR NOT EXISTS (SELECT 1 FROM freight.rates r WHERE r.id = brs.rate_id);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.booking_rate_snapshot
|
||||
ADD CONSTRAINT "FK_booking_rate_snapshot_booking_id"
|
||||
FOREIGN KEY (booking_id)
|
||||
REFERENCES freight.bookings(id)
|
||||
ON DELETE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.booking_rate_snapshot
|
||||
ADD CONSTRAINT "FK_booking_rate_snapshot_rate_id"
|
||||
FOREIGN KEY (rate_id)
|
||||
REFERENCES freight.rates(id)
|
||||
ON DELETE RESTRICT;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
// ── freight.booking_approval_step ─────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_approval_step bas
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = bas.booking_id)
|
||||
OR NOT EXISTS (SELECT 1 FROM freight.approval_rules ar WHERE ar.id = bas.approval_rule_id);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.booking_approval_step
|
||||
ADD CONSTRAINT "FK_booking_approval_step_booking_id"
|
||||
FOREIGN KEY (booking_id)
|
||||
REFERENCES freight.bookings(id)
|
||||
ON DELETE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.booking_approval_step
|
||||
ADD CONSTRAINT "FK_booking_approval_step_approval_rule_id"
|
||||
FOREIGN KEY (approval_rule_id)
|
||||
REFERENCES freight.approval_rules(id)
|
||||
ON DELETE RESTRICT;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
// ── freight.booking_cargo_modifier ────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_cargo_modifier bcm
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = bcm.booking_id)
|
||||
OR NOT EXISTS (SELECT 1 FROM freight.surcharge_types st WHERE st.id = bcm.surcharge_type_id)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM freight.booking_rate_snapshot brs WHERE brs.id = bcm.rate_snapshot_id
|
||||
);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.booking_cargo_modifier
|
||||
ADD CONSTRAINT "FK_booking_cargo_modifier_booking_id"
|
||||
FOREIGN KEY (booking_id)
|
||||
REFERENCES freight.bookings(id)
|
||||
ON DELETE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.booking_cargo_modifier
|
||||
ADD CONSTRAINT "FK_booking_cargo_modifier_surcharge_type_id"
|
||||
FOREIGN KEY (surcharge_type_id)
|
||||
REFERENCES freight.surcharge_types(id)
|
||||
ON DELETE RESTRICT;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.booking_cargo_modifier
|
||||
ADD CONSTRAINT "FK_booking_cargo_modifier_rate_snapshot_id"
|
||||
FOREIGN KEY (rate_snapshot_id)
|
||||
REFERENCES freight.booking_rate_snapshot(id)
|
||||
ON DELETE RESTRICT;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.booking_cargo_modifier
|
||||
DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_rate_snapshot_id";
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.booking_cargo_modifier
|
||||
DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_surcharge_type_id";
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.booking_cargo_modifier
|
||||
DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_booking_id";
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.booking_approval_step
|
||||
DROP CONSTRAINT IF EXISTS "FK_booking_approval_step_approval_rule_id";
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.booking_approval_step
|
||||
DROP CONSTRAINT IF EXISTS "FK_booking_approval_step_booking_id";
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.booking_rate_snapshot
|
||||
DROP CONSTRAINT IF EXISTS "FK_booking_rate_snapshot_rate_id";
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.booking_rate_snapshot
|
||||
DROP CONSTRAINT IF EXISTS "FK_booking_rate_snapshot_booking_id";
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.booking_container
|
||||
DROP CONSTRAINT IF EXISTS "FK_booking_container_weight_limit_rule_id";
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.booking_container
|
||||
DROP CONSTRAINT IF EXISTS "FK_booking_container_container_type_id";
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP CONSTRAINT IF EXISTS "FK_bookings_consolidation_partner_id";
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP CONSTRAINT IF EXISTS "FK_bookings_previous_contract_id";
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP CONSTRAINT IF EXISTS "FK_bookings_train_id";
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP CONSTRAINT IF EXISTS "FK_bookings_customer_id";
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class MoveCustomersToFreightSchema1748900000000 implements MigrationInterface {
|
||||
name = 'MoveCustomersToFreightSchema1748900000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.customers (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL,
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(150) NOT NULL UNIQUE,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
company_name VARCHAR(200) NOT NULL,
|
||||
company_email VARCHAR(150) NOT NULL,
|
||||
company_phone VARCHAR(20) NOT NULL,
|
||||
company_location VARCHAR(100) NOT NULL,
|
||||
company_address TEXT NOT NULL,
|
||||
customer_type VARCHAR(32),
|
||||
status VARCHAR(32),
|
||||
contact_person_name VARCHAR(100) NOT NULL,
|
||||
contact_person_phone VARCHAR(20) NOT NULL,
|
||||
tin_number VARCHAR(10) NOT NULL UNIQUE,
|
||||
vat_number VARCHAR(50),
|
||||
fan_number VARCHAR(16) NOT NULL UNIQUE,
|
||||
general_manager_name VARCHAR(100) NOT NULL,
|
||||
general_manager_email VARCHAR(150) NOT NULL,
|
||||
general_manager_phone VARCHAR(20) NOT NULL,
|
||||
poa_name VARCHAR(100),
|
||||
poa_phone VARCHAR(20),
|
||||
poa_address TEXT,
|
||||
poa_email VARCHAR(150),
|
||||
poa_location VARCHAR(100),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS "IDX_freight_customers_email"
|
||||
ON freight.customers (email);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS "IDX_freight_customers_user_id"
|
||||
ON freight.customers (user_id);
|
||||
`);
|
||||
|
||||
// Copy rows from public.customers when that legacy table exists
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
DECLARE
|
||||
has_public boolean;
|
||||
has_user_id boolean;
|
||||
has_userid boolean;
|
||||
BEGIN
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'customers'
|
||||
) INTO has_public;
|
||||
|
||||
IF NOT has_public THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'customers' AND column_name = 'user_id'
|
||||
) INTO has_user_id;
|
||||
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'customers' AND column_name = 'userid'
|
||||
) INTO has_userid;
|
||||
|
||||
IF has_user_id THEN
|
||||
INSERT INTO freight.customers (
|
||||
id, user_id, first_name, last_name, email, phone,
|
||||
company_name, company_email, company_phone, company_location, company_address,
|
||||
contact_person_name, contact_person_phone, tin_number, vat_number, fan_number,
|
||||
general_manager_name, general_manager_email, general_manager_phone,
|
||||
poa_name, poa_phone, poa_address, poa_email, poa_location, notes,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
id, user_id, first_name, last_name, email, phone,
|
||||
company_name, company_email, company_phone, company_location, company_address,
|
||||
contact_person_name, contact_person_phone, tin_number, vat_number, fan_number,
|
||||
general_manager_name, general_manager_email, general_manager_phone,
|
||||
poa_name, poa_phone, poa_address, poa_email, poa_location, notes,
|
||||
COALESCE(created_at, now()), COALESCE(updated_at, now())
|
||||
FROM public.customers
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
ELSIF has_userid THEN
|
||||
INSERT INTO freight.customers (
|
||||
id, user_id, first_name, last_name, email, phone,
|
||||
company_name, company_email, company_phone, company_location, company_address,
|
||||
contact_person_name, contact_person_phone, tin_number, vat_number, fan_number,
|
||||
general_manager_name, general_manager_email, general_manager_phone,
|
||||
poa_name, poa_phone, poa_address, poa_email, poa_location, notes,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
id, userid, firstname, lastname, email, phone,
|
||||
companyname, companyemail, companyphone, companylocation, companyaddress,
|
||||
contactpersonname, contactpersonphone, tinnumber, vatnumber, fannumber,
|
||||
generalmanagername, generalmanageremail, generalmanagerphone,
|
||||
poaname, poaphone, poaaddress, poaemail, poalocation, notes,
|
||||
COALESCE("createdAt", now()), COALESCE("updatedAt", now())
|
||||
FROM public.customers
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP CONSTRAINT IF EXISTS "FK_bookings_customer_id";
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_cargo_modifier bcm
|
||||
USING freight.bookings b
|
||||
WHERE bcm.booking_id = b.id
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_approval_step bas
|
||||
USING freight.bookings b
|
||||
WHERE bas.booking_id = b.id
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_rate_snapshot brs
|
||||
USING freight.bookings b
|
||||
WHERE brs.booking_id = b.id
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.booking_container bc
|
||||
USING freight.bookings b
|
||||
WHERE bc.booking_id = b.id
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.bookings b
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.bookings
|
||||
ADD CONSTRAINT "FK_bookings_customer_id"
|
||||
FOREIGN KEY (customer_id)
|
||||
REFERENCES freight.customers(id)
|
||||
ON DELETE RESTRICT;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP CONSTRAINT IF EXISTS "FK_bookings_customer_id";
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.bookings
|
||||
ADD CONSTRAINT "FK_bookings_customer_id"
|
||||
FOREIGN KEY (customer_id)
|
||||
REFERENCES public.customers(id)
|
||||
ON DELETE RESTRICT;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.customers CASCADE;`);
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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 } });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { PartialType } from "@nestjs/mapped-types";
|
||||
|
||||
import { CreateCustomerDto } from "./create-customer.dto";
|
||||
|
||||
export class UpdateCustomerDto extends PartialType(CreateCustomerDto) {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -2,12 +2,12 @@
|
||||
NODE_ENV=development
|
||||
PORT=3002
|
||||
|
||||
# Database
|
||||
# Database (local Docker: run `pnpm dev:db` from edr-platform, then copy to .env)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5434
|
||||
DB_NAME=edr_passenger
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=
|
||||
DB_PASSWORD=postgres
|
||||
|
||||
# JWT (provided by external auth package — placeholder only)
|
||||
JWT_SECRET=
|
||||
|
||||
Reference in New Issue
Block a user