diff --git a/apps/edr-freight-api/src/migrations/1748800000000-AddBookingsRemainingForeignKeys.ts b/apps/edr-freight-api/src/migrations/1748800000000-AddBookingsRemainingForeignKeys.ts new file mode 100644 index 000000000..72a751f4b --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1748800000000-AddBookingsRemainingForeignKeys.ts @@ -0,0 +1,321 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddBookingsRemainingForeignKeys1748800000000 implements MigrationInterface { + name = 'AddBookingsRemainingForeignKeys1748800000000'; + + public async up(queryRunner: QueryRunner): Promise { + // ── 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 { + 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"; + `); + } +} diff --git a/apps/edr-freight-api/src/migrations/1748900000000-MoveCustomersToFreightSchema.ts b/apps/edr-freight-api/src/migrations/1748900000000-MoveCustomersToFreightSchema.ts new file mode 100644 index 000000000..43bb0eb02 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1748900000000-MoveCustomersToFreightSchema.ts @@ -0,0 +1,185 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MoveCustomersToFreightSchema1748900000000 implements MigrationInterface { + name = 'MoveCustomersToFreightSchema1748900000000'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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;`); + } +} diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts index 60980952e..32147db7d 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts @@ -56,6 +56,8 @@ export class BookingsRepository extends BaseRepository { .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') diff --git a/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts b/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts index e7c456375..7875babbf 100644 --- a/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts +++ b/apps/edr-freight-api/src/modules/bookings/entities/booking.entity.ts @@ -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[]; diff --git a/apps/edr-freight-api/src/modules/customers/customers.repository.ts b/apps/edr-freight-api/src/modules/customers/customers.repository.ts index 83a64b9da..5933cef61 100644 --- a/apps/edr-freight-api/src/modules/customers/customers.repository.ts +++ b/apps/edr-freight-api/src/modules/customers/customers.repository.ts @@ -44,7 +44,7 @@ export class CustomersRepository { async findByName(name: string): Promise { return await this.repository .createQueryBuilder("customer") - .where("customer.name ILIKE :name", { name: `%${name}%` }) + .where("customer.companyName ILIKE :name", { name: `%${name}%` }) .getMany(); } diff --git a/apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts b/apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts index d6e9b9e17..98e350b96 100644 --- a/apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts +++ b/apps/edr-freight-api/src/modules/customers/dto/response-customer.dto.ts @@ -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; diff --git a/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts b/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts index 80041432e..fd2defac2 100644 --- a/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts +++ b/apps/edr-freight-api/src/modules/customers/entities/customer.entity.ts @@ -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; -} \ No newline at end of file + @Column({ name: 'notes', type: 'text', nullable: true }) + notes?: string | null; +} diff --git a/apps/edr-freight-api/src/modules/customers2/customers.controller.ts b/apps/edr-freight-api/src/modules/customers2/customers.controller.ts deleted file mode 100644 index 85b470453..000000000 --- a/apps/edr-freight-api/src/modules/customers2/customers.controller.ts +++ /dev/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); - } -} diff --git a/apps/edr-freight-api/src/modules/customers2/customers.module.ts b/apps/edr-freight-api/src/modules/customers2/customers.module.ts deleted file mode 100644 index 28c6b7c89..000000000 --- a/apps/edr-freight-api/src/modules/customers2/customers.module.ts +++ /dev/null @@ -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 {} diff --git a/apps/edr-freight-api/src/modules/customers2/customers.repository.ts b/apps/edr-freight-api/src/modules/customers2/customers.repository.ts deleted file mode 100644 index c6cb72fcf..000000000 --- a/apps/edr-freight-api/src/modules/customers2/customers.repository.ts +++ /dev/null @@ -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 { - constructor( - @InjectRepository(Customer) - repository: Repository, - ) { - super(repository); - } - - /** Find a customer by their unique email. */ - findByEmail(email: string): Promise { - return this.repository.findOne({ where: { email } }); - } -} diff --git a/apps/edr-freight-api/src/modules/customers2/customers.service.ts b/apps/edr-freight-api/src/modules/customers2/customers.service.ts deleted file mode 100644 index 6394e1ad9..000000000 --- a/apps/edr-freight-api/src/modules/customers2/customers.service.ts +++ /dev/null @@ -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 { - 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 { - return this.customersRepository.findAll({ order: { name: "ASC" } }); - } - - async findById(id: string): Promise { - 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 { - 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 { - await this.findById(id); - await this.customersRepository.softDelete(id); - } -} diff --git a/apps/edr-freight-api/src/modules/customers2/dto/create-customer.dto.ts b/apps/edr-freight-api/src/modules/customers2/dto/create-customer.dto.ts deleted file mode 100644 index 854b3eaf1..000000000 --- a/apps/edr-freight-api/src/modules/customers2/dto/create-customer.dto.ts +++ /dev/null @@ -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; -} diff --git a/apps/edr-freight-api/src/modules/customers2/dto/update-customer.dto.ts b/apps/edr-freight-api/src/modules/customers2/dto/update-customer.dto.ts deleted file mode 100644 index 499d0ef9b..000000000 --- a/apps/edr-freight-api/src/modules/customers2/dto/update-customer.dto.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PartialType } from "@nestjs/mapped-types"; - -import { CreateCustomerDto } from "./create-customer.dto"; - -export class UpdateCustomerDto extends PartialType(CreateCustomerDto) {} diff --git a/apps/edr-freight-api/src/modules/customers2/entities/customer.entity.ts b/apps/edr-freight-api/src/modules/customers2/entities/customer.entity.ts deleted file mode 100644 index 98a248c97..000000000 --- a/apps/edr-freight-api/src/modules/customers2/entities/customer.entity.ts +++ /dev/null @@ -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; -} diff --git a/apps/edr-passenger-api/.env.example b/apps/edr-passenger-api/.env.example index eba901b1e..6b42f0f42 100644 --- a/apps/edr-passenger-api/.env.example +++ b/apps/edr-passenger-api/.env.example @@ -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=