Files
edr-platform/apps/edr-freight-api/src/migrations/1821000000002-CreateInvoices.ts

186 lines
7.2 KiB
TypeScript

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> {
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<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;`,
);
}
}