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 (`__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 { const typeExists = await queryRunner.query( `SELECT 1 FROM pg_type WHERE typname = 'invoices_status_enum' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'freight');`, ); if (!typeExists.length) { await queryRunner.query(` CREATE TYPE freight.invoices_status_enum AS ENUM ( 'DRAFT', 'PENDING', 'PAID', 'OVERDUE', 'CANCELLED', 'REFUNDED' ); `); } await queryRunner.query(` CREATE TABLE IF NOT EXISTS 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( ` ALTER TABLE freight.invoices ADD COLUMN IF NOT EXISTS id uuid DEFAULT uuid_generate_v4(), ADD COLUMN IF NOT EXISTS invoice_number varchar(64), ADD COLUMN IF NOT EXISTS company_id uuid, ADD COLUMN IF NOT EXISTS company_profile_id uuid, ADD COLUMN IF NOT EXISTS total_amount numeric(14, 2) NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS currency varchar(8) NOT NULL DEFAULT 'ETB', ADD COLUMN IF NOT EXISTS status freight.invoices_status_enum NOT NULL DEFAULT 'DRAFT', ADD COLUMN IF NOT EXISTS source varchar(255), ADD COLUMN IF NOT EXISTS source_id varchar(255), ADD COLUMN IF NOT EXISTS type varchar(255), ADD COLUMN IF NOT EXISTS issued_at timestamptz, ADD COLUMN IF NOT EXISTS payment_id uuid, ADD COLUMN IF NOT EXISTS due_at timestamptz, ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now(), ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now(), ADD COLUMN IF NOT EXISTS deleted_at timestamptz; `, ); await queryRunner.query(` UPDATE freight.invoices SET due_at = COALESCE(due_at, issued_at, created_at, now()) WHERE due_at IS NULL; `); await queryRunner.query(`ALTER TABLE freight.invoices ALTER COLUMN due_at SET NOT NULL;`); await queryRunner.query(` DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE contype = 'p' AND conrelid = 'freight.invoices'::regclass ) THEN ALTER TABLE freight.invoices ADD CONSTRAINT pk_invoices PRIMARY KEY (id); END IF; IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'uq_invoices_invoice_number' AND conrelid = 'freight.invoices'::regclass ) THEN ALTER TABLE freight.invoices ADD CONSTRAINT uq_invoices_invoice_number UNIQUE (invoice_number); END IF; IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'fk_invoices_company' AND conrelid = 'freight.invoices'::regclass ) THEN ALTER TABLE freight.invoices ADD CONSTRAINT fk_invoices_company FOREIGN KEY (company_id) REFERENCES freight.companies (id) ON DELETE RESTRICT; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'fk_invoices_company_profile' AND conrelid = 'freight.invoices'::regclass ) THEN ALTER TABLE freight.invoices ADD CONSTRAINT fk_invoices_company_profile FOREIGN KEY (company_profile_id) REFERENCES freight.company_profiles (id) ON DELETE RESTRICT; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'fk_invoices_payment' AND conrelid = 'freight.invoices'::regclass ) THEN ALTER TABLE freight.invoices ADD CONSTRAINT fk_invoices_payment FOREIGN KEY (payment_id) REFERENCES freight.payments (id) ON DELETE SET NULL; END IF; END $$; `); await queryRunner.query( `CREATE INDEX IF NOT EXISTS idx_invoices_company ON freight.invoices (company_id);`, ); await queryRunner.query( `CREATE INDEX IF NOT EXISTS idx_invoices_company_profile ON freight.invoices (company_profile_id);`, ); await queryRunner.query( `CREATE INDEX IF NOT EXISTS idx_invoices_source ON freight.invoices (source, source_id);`, ); await queryRunner.query( `CREATE INDEX IF NOT EXISTS idx_invoices_status ON freight.invoices (status);`, ); await queryRunner.query(` CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS idx_invoice_lines_invoice ON freight.invoice_lines (invoice_id);`, ); } public async down(queryRunner: QueryRunner): Promise { 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;`, ); } }