From 14dde418f7ab43802b46c765a9400cb09b3ac159 Mon Sep 17 00:00:00 2001 From: natib21 Date: Tue, 30 Jun 2026 03:55:57 +0000 Subject: [PATCH 1/3] fix(migrations): check if tables exist before CREATE TABLE Make invoices migration idempotent - skip table/index creation if they already exist. Prevents 'relation already exists' errors on redeployment. Co-Authored-By: Claude Haiku 4.5 --- .../1821000000002-CreateInvoices.ts | 134 ++++++++++-------- 1 file changed, 73 insertions(+), 61 deletions(-) diff --git a/apps/edr-freight-api/src/migrations/1821000000002-CreateInvoices.ts b/apps/edr-freight-api/src/migrations/1821000000002-CreateInvoices.ts index 93196578d..a36ffa5da 100644 --- a/apps/edr-freight-api/src/migrations/1821000000002-CreateInvoices.ts +++ b/apps/edr-freight-api/src/migrations/1821000000002-CreateInvoices.ts @@ -32,71 +32,83 @@ export class CreateInvoices1821000000002 implements MigrationInterface { `); } - await queryRunner.query(` - CREATE TABLE freight.invoices ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - invoice_number varchar(64) NOT NULL, - company_id uuid NOT NULL, - company_profile_id uuid NOT NULL, - total_amount numeric(14, 2) NOT NULL, - currency varchar(8) NOT NULL DEFAULT 'ETB', - status freight.invoices_status_enum NOT NULL DEFAULT 'DRAFT', - source varchar(255) NOT NULL, - source_id varchar(255) NOT NULL, - type varchar(255) NOT NULL, - issued_at timestamptz, - payment_id uuid, - due_at timestamptz NOT NULL, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT pk_invoices PRIMARY KEY (id), - CONSTRAINT uq_invoices_invoice_number UNIQUE (invoice_number), - CONSTRAINT fk_invoices_company FOREIGN KEY (company_id) - REFERENCES freight.companies (id) ON DELETE RESTRICT, - CONSTRAINT fk_invoices_company_profile FOREIGN KEY (company_profile_id) - REFERENCES freight.company_profiles (id) ON DELETE RESTRICT, - CONSTRAINT fk_invoices_payment FOREIGN KEY (payment_id) - REFERENCES freight.payments (id) ON DELETE SET NULL + const invoicesExists = await queryRunner.query( + `SELECT 1 FROM information_schema.tables WHERE table_schema = 'freight' AND table_name = 'invoices';`, + ); + + if (!invoicesExists.length) { + await queryRunner.query(` + CREATE TABLE freight.invoices ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + invoice_number varchar(64) NOT NULL, + company_id uuid NOT NULL, + company_profile_id uuid NOT NULL, + total_amount numeric(14, 2) NOT NULL, + currency varchar(8) NOT NULL DEFAULT 'ETB', + status freight.invoices_status_enum NOT NULL DEFAULT 'DRAFT', + source varchar(255) NOT NULL, + source_id varchar(255) NOT NULL, + type varchar(255) NOT NULL, + issued_at timestamptz, + payment_id uuid, + due_at timestamptz NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz, + CONSTRAINT pk_invoices PRIMARY KEY (id), + CONSTRAINT uq_invoices_invoice_number UNIQUE (invoice_number), + CONSTRAINT fk_invoices_company FOREIGN KEY (company_id) + REFERENCES freight.companies (id) ON DELETE RESTRICT, + CONSTRAINT fk_invoices_company_profile FOREIGN KEY (company_profile_id) + REFERENCES freight.company_profiles (id) ON DELETE RESTRICT, + CONSTRAINT fk_invoices_payment FOREIGN KEY (payment_id) + REFERENCES freight.payments (id) ON DELETE SET NULL + ); + `); + + await queryRunner.query( + `CREATE INDEX idx_invoices_company ON freight.invoices (company_id);`, ); - `); - - await queryRunner.query( - `CREATE INDEX idx_invoices_company ON freight.invoices (company_id);`, - ); - await queryRunner.query( - `CREATE INDEX idx_invoices_company_profile ON freight.invoices (company_profile_id);`, - ); - await queryRunner.query( - `CREATE INDEX idx_invoices_source ON freight.invoices (source, source_id);`, - ); - await queryRunner.query( - `CREATE INDEX idx_invoices_status ON freight.invoices (status);`, - ); - - await queryRunner.query(` - CREATE TABLE freight.invoice_lines ( - id uuid NOT NULL DEFAULT uuid_generate_v4(), - invoice_id uuid NOT NULL, - charge_type varchar NOT NULL, - description varchar(255), - quantity numeric(12, 2) NOT NULL DEFAULT 1, - unit_rate numeric(14, 2) NOT NULL DEFAULT 0, - amount numeric(14, 2) NOT NULL, - currency varchar(8) NOT NULL DEFAULT 'ETB', - metadata jsonb, - created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - deleted_at timestamptz, - CONSTRAINT pk_invoice_lines PRIMARY KEY (id), - CONSTRAINT fk_invoice_lines_invoice FOREIGN KEY (invoice_id) - REFERENCES freight.invoices (id) ON DELETE CASCADE + await queryRunner.query( + `CREATE INDEX idx_invoices_company_profile ON freight.invoices (company_profile_id);`, ); - `); + await queryRunner.query( + `CREATE INDEX idx_invoices_source ON freight.invoices (source, source_id);`, + ); + await queryRunner.query( + `CREATE INDEX idx_invoices_status ON freight.invoices (status);`, + ); + } - await queryRunner.query( - `CREATE INDEX idx_invoice_lines_invoice ON freight.invoice_lines (invoice_id);`, + const invoiceLinesExists = await queryRunner.query( + `SELECT 1 FROM information_schema.tables WHERE table_schema = 'freight' AND table_name = 'invoice_lines';`, ); + + if (!invoiceLinesExists.length) { + await queryRunner.query(` + CREATE TABLE freight.invoice_lines ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + invoice_id uuid NOT NULL, + charge_type varchar NOT NULL, + description varchar(255), + quantity numeric(12, 2) NOT NULL DEFAULT 1, + unit_rate numeric(14, 2) NOT NULL DEFAULT 0, + amount numeric(14, 2) NOT NULL, + currency varchar(8) NOT NULL DEFAULT 'ETB', + metadata jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz, + CONSTRAINT pk_invoice_lines PRIMARY KEY (id), + CONSTRAINT fk_invoice_lines_invoice FOREIGN KEY (invoice_id) + REFERENCES freight.invoices (id) ON DELETE CASCADE + ); + `); + + await queryRunner.query( + `CREATE INDEX idx_invoice_lines_invoice ON freight.invoice_lines (invoice_id);`, + ); + } } public async down(queryRunner: QueryRunner): Promise { From 0bc95341720c116bd962e94f703e3f1c949f47de Mon Sep 17 00:00:00 2001 From: natib21 Date: Tue, 30 Jun 2026 04:03:29 +0000 Subject: [PATCH 2/3] fix: replace apiClient with api in FirstMilePage Use correct import 'api' from '@/auth/http' instead of undefined 'apiClient'. Co-Authored-By: Claude Haiku 4.5 --- .../backoffice/src/pages/operations/FirstMilePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 569511d30..164d88f0a 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -441,7 +441,7 @@ const FirstMilePage = () => { }); const allocateMutation = useMutation({ - mutationFn: (data) => apiClient.post(`/first-mile/${containerAllocationFirstMileId}/allocate-containers`, data), + mutationFn: (data) => api.post(`/first-mile/${containerAllocationFirstMileId}/allocate-containers`, data), onSuccess: () => { toast({ title: "Containers allocated" }); void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.detail(containerAllocationFirstMileId ?? "") }); From 360e61ac15b07ab5da2691d11dd776fcd5030e71 Mon Sep 17 00:00:00 2001 From: natib21 Date: Tue, 30 Jun 2026 04:04:56 +0000 Subject: [PATCH 3/3] fix: use byId() instead of detail() in QUERY_KEYS QUERY_KEYS.FIRST_MILE and QUERY_KEYS.LAST_MILE have byId() not detail(). Co-Authored-By: Claude Haiku 4.5 --- .../backoffice/src/pages/operations/FirstMilePage.tsx | 2 +- .../backoffice/src/pages/operations/LastMilePage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx index 164d88f0a..52b3d7139 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/FirstMilePage.tsx @@ -444,7 +444,7 @@ const FirstMilePage = () => { mutationFn: (data) => api.post(`/first-mile/${containerAllocationFirstMileId}/allocate-containers`, data), onSuccess: () => { toast({ title: "Containers allocated" }); - void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.detail(containerAllocationFirstMileId ?? "") }); + void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.byId(containerAllocationFirstMileId ?? "") }); setContainerAllocationOpen(false); setContainerAllocationFirstMileId(null); }, diff --git a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx index 3de5e578e..a40721a1c 100644 --- a/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/operations/LastMilePage.tsx @@ -395,7 +395,7 @@ const LastMilePage = () => { api.post(`/last-mile/${activeId}/allocate-containers`, data), onSuccess: () => { toast({ title: "Containers allocated", variant: "default" }); - void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.detail(activeId ?? "") }); + void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.byId(activeId ?? "") }); closeAllocation(); }, onError: () => {