mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
fix
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Freight billing — `invoices` + `invoice_lines` tables.
|
||||
*
|
||||
* Matches:
|
||||
* - billing/entities/invoice.entity.ts
|
||||
* - billing/entities/invoice-line.entity.ts
|
||||
*
|
||||
* The status enum mirrors `Freight.InvoiceStatus` and uses TypeORM's default
|
||||
* enum-type name (`<table>_<column>_enum`) so the entity's `type: "enum"`
|
||||
* column resolves to it without an explicit `enumName`.
|
||||
*/
|
||||
export class CreateInvoices1821000000002 implements MigrationInterface {
|
||||
name = "CreateInvoices1821000000002";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TYPE freight.invoices_status_enum AS ENUM (
|
||||
'DRAFT',
|
||||
'PENDING',
|
||||
'PAID',
|
||||
'OVERDUE',
|
||||
'CANCELLED',
|
||||
'REFUNDED'
|
||||
);
|
||||
`);
|
||||
|
||||
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_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_invoice_lines_invoice ON freight.invoice_lines (invoice_id);`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.invoice_lines;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.invoices;`);
|
||||
await queryRunner.query(`DROP TYPE IF EXISTS freight.invoices_status_enum;`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Government bookings now bill to a real seeded government company + an explicit
|
||||
* importer/exporter profile, instead of carrying a null company + free-text
|
||||
* institution. This migration:
|
||||
*
|
||||
* 1. Adds `companies.kind` (commercial | government).
|
||||
* 2. Seeds the Ethiopian government entities + their importer/exporter
|
||||
* profiles (mirrors src/seed/data/gov-companies.data.ts — keep in sync).
|
||||
* 3. Backfills every booking with a NULL company_id / company_profile_id so
|
||||
* the NOT NULL constraints below can be applied:
|
||||
* - NULL company_id → the default government company.
|
||||
* - NULL company_profile_id → the company's profile matching the booking
|
||||
* trade direction; else any profile of the company; else the default
|
||||
* government importer profile.
|
||||
* 4. Enforces NOT NULL on bookings.company_id and bookings.company_profile_id.
|
||||
*/
|
||||
export class AddCompanyKindAndGovBookingLinks1821000000003
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = "AddCompanyKindAndGovBookingLinks1821000000003";
|
||||
|
||||
// Mirrors src/seed/data/gov-companies.data.ts
|
||||
private readonly govCompanies = [
|
||||
{ id: "0a1b0001-0000-4000-8000-000000000001", name: "Federal Government of Ethiopia", tin: "0000000001", email: "procurement@gov.et", phone: "+251111000001", im: "0b1c0001-0000-4000-8000-000000000001", ex: "0b1c0001-0000-4000-8000-000000000002", imRef: "IM-90001", exRef: "EX-90001" },
|
||||
{ id: "0a1b0002-0000-4000-8000-000000000002", name: "Ministry of National Defense", tin: "0000000002", email: "logistics@mod.gov.et", phone: "+251111000002", im: "0b1c0002-0000-4000-8000-000000000001", ex: "0b1c0002-0000-4000-8000-000000000002", imRef: "IM-90002", exRef: "EX-90002" },
|
||||
{ id: "0a1b0003-0000-4000-8000-000000000003", name: "Ethiopian Roads Administration", tin: "0000000003", email: "supply@era.gov.et", phone: "+251111000003", im: "0b1c0003-0000-4000-8000-000000000001", ex: "0b1c0003-0000-4000-8000-000000000002", imRef: "IM-90003", exRef: "EX-90003" },
|
||||
{ id: "0a1b0004-0000-4000-8000-000000000004", name: "Ministry of Agriculture", tin: "0000000004", email: "imports@moa.gov.et", phone: "+251111000004", im: "0b1c0004-0000-4000-8000-000000000001", ex: "0b1c0004-0000-4000-8000-000000000002", imRef: "IM-90004", exRef: "EX-90004" },
|
||||
{ id: "0a1b0005-0000-4000-8000-000000000005", name: "Ministry of Trade and Regional Integration", tin: "0000000005", email: "trade@motri.gov.et", phone: "+251111000005", im: "0b1c0005-0000-4000-8000-000000000001", ex: "0b1c0005-0000-4000-8000-000000000002", imRef: "IM-90005", exRef: "EX-90005" },
|
||||
{ id: "0a1b0006-0000-4000-8000-000000000006", name: "Ethiopian Disaster Risk Management Commission", tin: "0000000006", email: "relief@edrmc.gov.et", phone: "+251111000006", im: "0b1c0006-0000-4000-8000-000000000001", ex: "0b1c0006-0000-4000-8000-000000000002", imRef: "IM-90006", exRef: "EX-90006" },
|
||||
];
|
||||
|
||||
private get defaultCompanyId(): string {
|
||||
return this.govCompanies[0].id;
|
||||
}
|
||||
private get defaultImporterProfileId(): string {
|
||||
return this.govCompanies[0].im;
|
||||
}
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// 1. kind column
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "freight"."companies" ADD COLUMN IF NOT EXISTS "kind" varchar(20) NOT NULL DEFAULT 'commercial'`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX IF NOT EXISTS "IDX_companies_kind" ON "freight"."companies" ("kind")`,
|
||||
);
|
||||
|
||||
// 2. seed government companies + importer/exporter profiles (idempotent)
|
||||
for (const g of this.govCompanies) {
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "freight"."companies" ("id", "name", "type", "kind", "status", "tin", "country", "email", "phone")
|
||||
VALUES ($1, $2, 'customer', 'government', 'active', $3, 'Ethiopia', $4, $5)
|
||||
ON CONFLICT ("id") DO NOTHING`,
|
||||
[g.id, g.name, g.tin, g.email, g.phone],
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "freight"."company_profiles" ("id", "company_id", "type", "reference", "status")
|
||||
VALUES ($1, $2, 'importer', $3, 'active'), ($4, $2, 'exporter', $5, 'active')
|
||||
ON CONFLICT ("id") DO NOTHING`,
|
||||
[g.im, g.id, g.imRef, g.ex, g.exRef],
|
||||
);
|
||||
}
|
||||
|
||||
// 3a. backfill NULL company_id → default government company
|
||||
await queryRunner.query(
|
||||
`UPDATE "freight"."bookings" SET "company_id" = $1 WHERE "company_id" IS NULL`,
|
||||
[this.defaultCompanyId],
|
||||
);
|
||||
|
||||
// 3b. backfill NULL company_profile_id → profile matching trade direction
|
||||
await queryRunner.query(
|
||||
`UPDATE "freight"."bookings" b
|
||||
SET "company_profile_id" = cp."id"
|
||||
FROM "freight"."company_profiles" cp
|
||||
WHERE b."company_profile_id" IS NULL
|
||||
AND cp."company_id" = b."company_id"
|
||||
AND cp."deleted_at" IS NULL
|
||||
AND cp."type" = CASE b."trade_direction"
|
||||
WHEN 'IMPORT' THEN 'importer'
|
||||
WHEN 'EXPORT' THEN 'exporter'
|
||||
ELSE NULL END`,
|
||||
);
|
||||
|
||||
// 3c. fallback → any profile of the booking's company
|
||||
await queryRunner.query(
|
||||
`UPDATE "freight"."bookings" b
|
||||
SET "company_profile_id" = (
|
||||
SELECT cp."id" FROM "freight"."company_profiles" cp
|
||||
WHERE cp."company_id" = b."company_id" AND cp."deleted_at" IS NULL
|
||||
ORDER BY cp."created_at" ASC LIMIT 1)
|
||||
WHERE b."company_profile_id" IS NULL
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM "freight"."company_profiles" cp
|
||||
WHERE cp."company_id" = b."company_id" AND cp."deleted_at" IS NULL)`,
|
||||
);
|
||||
|
||||
// 3d. final fallback → default government importer profile
|
||||
await queryRunner.query(
|
||||
`UPDATE "freight"."bookings" SET "company_profile_id" = $1 WHERE "company_profile_id" IS NULL`,
|
||||
[this.defaultImporterProfileId],
|
||||
);
|
||||
|
||||
// 4. enforce NOT NULL
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "freight"."bookings" ALTER COLUMN "company_id" SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "freight"."bookings" ALTER COLUMN "company_profile_id" SET NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "freight"."bookings" ALTER COLUMN "company_profile_id" DROP NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "freight"."bookings" ALTER COLUMN "company_id" DROP NOT NULL`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS "freight"."IDX_companies_kind"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "freight"."companies" DROP COLUMN IF EXISTS "kind"`,
|
||||
);
|
||||
// Seeded government rows are intentionally left in place.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Make the payment projection source-agnostic so any domain (not just bookings)
|
||||
* can own a payment intent.
|
||||
*
|
||||
* - `payments.type` enum `('booking')` → `varchar(50)`. It now stores the
|
||||
* invoice SOURCE (e.g. 'booking', 'demurrage'), supplied by the caller, so a
|
||||
* new domain no longer needs an enum migration to write its intents.
|
||||
* - adds `payments.reference_type varchar(40)` — the gateway reference type
|
||||
* (`PaymentReferenceType`) the intent was opened with, so the reconcile/poll
|
||||
* path can query the provider without hardcoding it.
|
||||
*
|
||||
* Matches payment/entities/payment.entity.ts.
|
||||
*/
|
||||
export class MakePaymentsTypeGeneric1821000000004 implements MigrationInterface {
|
||||
name = "MakePaymentsTypeGeneric1821000000004";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.payments ALTER COLUMN type TYPE varchar(50) USING type::text;`,
|
||||
);
|
||||
await queryRunner.query(`DROP TYPE IF EXISTS freight.payments_type_enum;`);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.payments ADD COLUMN reference_type varchar(40);`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.payments DROP COLUMN IF EXISTS reference_type;`,
|
||||
);
|
||||
|
||||
// Restore the single-value enum. Any non-'booking' rows would block the cast;
|
||||
// collapse them first so the down migration is safe.
|
||||
await queryRunner.query(
|
||||
`UPDATE freight.payments SET type = 'booking' WHERE type <> 'booking';`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE freight.payments_type_enum AS ENUM ('booking');`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.payments ALTER COLUMN type TYPE freight.payments_type_enum USING type::freight.payments_type_enum;`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user