import { MigrationInterface, QueryRunner } from "typeorm"; /** * Extend `freight.invoices` into the billing record of record for every source * (booking, demurrage, warehouse fees, …) so warehouse fee invoices can be * centralized onto it instead of the parallel `warehouse_fee_invoices` table. * * Adds money tracking that supports partial payment (`subtotal/tax/paid/balance`), * a `paid_at` stamp, a `payments` jsonb ledger, and the `ISSUED` / `PARTIALLY_PAID` * statuses the warehouse flow uses. * * Matches billing/entities/invoice.entity.ts. All columns are additive with * defaults, so existing booking/demurrage rows are unaffected. */ export class ExtendInvoicesForPartialPayment1828000000000 implements MigrationInterface { name = "ExtendInvoicesForPartialPayment1828000000000"; public async up(queryRunner: QueryRunner): Promise { // New statuses. ADD VALUE is non-transactional-value-safe on PG 12+ as long // as the value is not referenced in the same transaction (it is not here). await queryRunner.query( `ALTER TYPE freight.invoices_status_enum ADD VALUE IF NOT EXISTS 'ISSUED' BEFORE 'PENDING';`, ); await queryRunner.query( `ALTER TYPE freight.invoices_status_enum ADD VALUE IF NOT EXISTS 'PARTIALLY_PAID' BEFORE 'PAID';`, ); await queryRunner.query(` ALTER TABLE freight.invoices ADD COLUMN IF NOT EXISTS subtotal_amount numeric(14, 2) NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS tax_amount numeric(14, 2) NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS paid_amount numeric(14, 2) NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS balance_amount numeric(14, 2) NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS paid_at timestamptz, ADD COLUMN IF NOT EXISTS payments jsonb NOT NULL DEFAULT '[]'; `); // Backfill existing rows: subtotal mirrors the total (no tax was modeled), // the outstanding balance is the full total for unpaid invoices. await queryRunner.query(` UPDATE freight.invoices SET subtotal_amount = total_amount, balance_amount = total_amount; `); // Already-settled invoices: fully paid, zero balance, stamped from updated_at. await queryRunner.query(` UPDATE freight.invoices SET paid_amount = total_amount, balance_amount = 0, paid_at = updated_at WHERE status = 'PAID'; `); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE freight.invoices DROP COLUMN IF EXISTS payments, DROP COLUMN IF EXISTS paid_at, DROP COLUMN IF EXISTS balance_amount, DROP COLUMN IF EXISTS paid_amount, DROP COLUMN IF EXISTS tax_amount, DROP COLUMN IF EXISTS subtotal_amount; `); // Postgres cannot drop individual enum values; ISSUED / PARTIALLY_PAID are // left on freight.invoices_status_enum (harmless, unused after down). } }