mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 02:30:55 +00:00
248 lines
10 KiB
TypeScript
248 lines
10 KiB
TypeScript
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
|
/**
|
|
* Fold warehouse fee invoices into the central billing system.
|
|
*
|
|
* Warehouse fee invoices are no longer a standalone aggregate: each becomes a
|
|
* global `freight.invoices` row (`source = 'warehouse'`, `source_id =
|
|
* inventory_id`) with its items as `freight.invoice_lines`. The warehouse
|
|
* service is now a thin layer over `BillingService`. This migration backfills the
|
|
* existing rows (preserving ids, numbers, status, amounts and payment history),
|
|
* then drops the two legacy tables.
|
|
*
|
|
* Rows that cannot be billed centrally — no company to bill (`company_id` /
|
|
* `company_profile_id` underivable from the customer or the booking) — are not
|
|
* migrated; they could never have been charged through the gateway and are
|
|
* dropped with the table.
|
|
*/
|
|
export class CentralizeWarehouseInvoices1829000000000 implements MigrationInterface {
|
|
name = 'CentralizeWarehouseInvoices1829000000000';
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'freight'
|
|
AND table_name = 'invoices'
|
|
AND column_name = 'booking_id'
|
|
) THEN
|
|
ALTER TABLE freight.invoices ALTER COLUMN booking_id DROP NOT NULL;
|
|
END IF;
|
|
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'freight'
|
|
AND table_name = 'invoices'
|
|
AND column_name = 'amount'
|
|
) THEN
|
|
ALTER TABLE freight.invoices ALTER COLUMN amount DROP NOT NULL;
|
|
END IF;
|
|
END $$;
|
|
`);
|
|
|
|
// 1. Invoice headers. Keep the same id so items still link, and so any
|
|
// external reference to the invoice id stays valid.
|
|
await queryRunner.query(`
|
|
INSERT INTO freight.invoices (
|
|
id, invoice_number, company_id, company_profile_id,
|
|
subtotal_amount, tax_amount, total_amount, paid_amount, balance_amount,
|
|
currency, status, source, source_id, type,
|
|
issued_at, paid_at, payments, payment_id, due_at,
|
|
created_at, updated_at, deleted_at
|
|
)
|
|
SELECT
|
|
fee.id,
|
|
fee.invoice_number,
|
|
COALESCE(fee.customer_id, b.company_id),
|
|
COALESCE(
|
|
b.company_profile_id,
|
|
(SELECT cp.id
|
|
FROM freight.company_profiles cp
|
|
WHERE cp.company_id = COALESCE(fee.customer_id, b.company_id)
|
|
AND cp.deleted_at IS NULL
|
|
ORDER BY cp.created_at ASC
|
|
LIMIT 1)
|
|
),
|
|
fee.subtotal_amount, fee.tax_amount, fee.total_amount, fee.paid_amount, fee.balance_amount,
|
|
fee.currency,
|
|
fee.status::freight.invoices_status_enum,
|
|
'warehouse',
|
|
fee.inventory_id,
|
|
fee.invoice_type,
|
|
fee.issued_at,
|
|
fee.paid_at,
|
|
COALESCE(fee.payments, '[]'::jsonb),
|
|
NULL,
|
|
COALESCE(fee.due_date, fee.issued_at, fee.created_at),
|
|
fee.created_at, fee.updated_at, fee.deleted_at
|
|
FROM freight.warehouse_fee_invoices fee
|
|
LEFT JOIN freight.bookings b ON b.id = fee.booking_id
|
|
WHERE COALESCE(fee.customer_id, b.company_id) IS NOT NULL
|
|
AND COALESCE(
|
|
b.company_profile_id,
|
|
(SELECT cp.id
|
|
FROM freight.company_profiles cp
|
|
WHERE cp.company_id = COALESCE(fee.customer_id, b.company_id)
|
|
AND cp.deleted_at IS NULL
|
|
ORDER BY cp.created_at ASC
|
|
LIMIT 1)
|
|
) IS NOT NULL
|
|
ON CONFLICT (id) DO NOTHING;
|
|
`);
|
|
|
|
// 2. Invoice lines — only for items whose parent invoice migrated. Warehouse
|
|
// fee fields (fee_rule_id / chargeable_days / free_days) move into the
|
|
// line's jsonb metadata.
|
|
await queryRunner.query(`
|
|
INSERT INTO freight.invoice_lines (
|
|
id, invoice_id, charge_type, description, quantity, unit_rate, amount,
|
|
currency, metadata, created_at, updated_at, deleted_at
|
|
)
|
|
SELECT
|
|
item.id,
|
|
item.invoice_id,
|
|
item.fee_type,
|
|
item.description,
|
|
item.quantity,
|
|
item.unit_rate,
|
|
item.amount,
|
|
item.currency,
|
|
jsonb_build_object(
|
|
'feeRuleId', item.fee_rule_id,
|
|
'chargeableDays', item.chargeable_days,
|
|
'freeDays', item.free_days
|
|
),
|
|
item.created_at, item.updated_at, item.deleted_at
|
|
FROM freight.warehouse_fee_invoice_items item
|
|
JOIN freight.invoices i ON i.id = item.invoice_id AND i.source = 'warehouse'
|
|
ON CONFLICT (id) DO NOTHING;
|
|
`);
|
|
|
|
// 3. Drop the legacy tables (items first — FK to invoices).
|
|
await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_fee_invoice_items;`);
|
|
await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_fee_invoices;`);
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
// Recreate the legacy tables …
|
|
await queryRunner.query(`
|
|
CREATE TABLE IF NOT EXISTS freight.warehouse_fee_invoices (
|
|
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
deleted_at timestamptz,
|
|
invoice_number varchar(40) NOT NULL,
|
|
booking_id uuid,
|
|
customer_id uuid,
|
|
inventory_id uuid NOT NULL,
|
|
facility_id uuid,
|
|
warehouse_id uuid,
|
|
yard_id uuid,
|
|
zone_id uuid,
|
|
invoice_type varchar(32) NOT NULL DEFAULT 'MIXED_WAREHOUSE_FEES',
|
|
status varchar(20) NOT NULL DEFAULT 'DRAFT',
|
|
subtotal_amount numeric(14,2) NOT NULL DEFAULT 0,
|
|
tax_amount numeric(14,2) NOT NULL DEFAULT 0,
|
|
total_amount numeric(14,2) NOT NULL DEFAULT 0,
|
|
paid_amount numeric(14,2) NOT NULL DEFAULT 0,
|
|
balance_amount numeric(14,2) NOT NULL DEFAULT 0,
|
|
currency varchar(8) NOT NULL DEFAULT 'USD',
|
|
period_start timestamptz,
|
|
period_end timestamptz,
|
|
issued_at timestamptz,
|
|
due_date timestamptz,
|
|
paid_at timestamptz,
|
|
cancelled_at timestamptz,
|
|
payments jsonb NOT NULL DEFAULT '[]',
|
|
notes text,
|
|
CONSTRAINT "PK_warehouse_fee_invoices" PRIMARY KEY (id),
|
|
CONSTRAINT "UQ_warehouse_fee_invoices_invoice_number" UNIQUE (invoice_number)
|
|
);
|
|
`);
|
|
await queryRunner.query(
|
|
`CREATE INDEX IF NOT EXISTS "IDX_warehouse_fee_invoices_booking_id" ON freight.warehouse_fee_invoices (booking_id);`,
|
|
);
|
|
await queryRunner.query(
|
|
`CREATE INDEX IF NOT EXISTS "IDX_warehouse_fee_invoices_inventory_id" ON freight.warehouse_fee_invoices (inventory_id);`,
|
|
);
|
|
await queryRunner.query(
|
|
`CREATE INDEX IF NOT EXISTS "IDX_warehouse_fee_invoices_status" ON freight.warehouse_fee_invoices (status);`,
|
|
);
|
|
await queryRunner.query(`
|
|
CREATE TABLE IF NOT EXISTS freight.warehouse_fee_invoice_items (
|
|
id uuid NOT NULL DEFAULT uuid_generate_v4(),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
deleted_at timestamptz,
|
|
invoice_id uuid NOT NULL,
|
|
fee_rule_id uuid,
|
|
fee_type varchar(32) NOT NULL,
|
|
description varchar(255) NOT NULL,
|
|
quantity numeric(12,2) NOT NULL DEFAULT 1,
|
|
unit_rate numeric(14,2) NOT NULL DEFAULT 0,
|
|
amount numeric(14,2) NOT NULL DEFAULT 0,
|
|
currency varchar(8) NOT NULL DEFAULT 'USD',
|
|
chargeable_days int,
|
|
free_days int,
|
|
CONSTRAINT "PK_warehouse_fee_invoice_items" PRIMARY KEY (id),
|
|
CONSTRAINT "FK_warehouse_fee_invoice_items_invoice"
|
|
FOREIGN KEY (invoice_id) REFERENCES freight.warehouse_fee_invoices (id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
await queryRunner.query(
|
|
`CREATE INDEX IF NOT EXISTS "IDX_warehouse_fee_invoice_items_invoice_id" ON freight.warehouse_fee_invoice_items (invoice_id);`,
|
|
);
|
|
|
|
// … then copy the warehouse-source invoices back, deriving the typed FKs and
|
|
// period from the linked inventory item.
|
|
await queryRunner.query(`
|
|
INSERT INTO freight.warehouse_fee_invoices (
|
|
id, created_at, updated_at, deleted_at, invoice_number,
|
|
booking_id, customer_id, inventory_id, facility_id, warehouse_id, yard_id, zone_id,
|
|
invoice_type, status, subtotal_amount, tax_amount, total_amount, paid_amount, balance_amount,
|
|
currency, period_start, period_end, issued_at, due_date, paid_at, cancelled_at, payments, notes
|
|
)
|
|
SELECT
|
|
i.id, i.created_at, i.updated_at, i.deleted_at, i.invoice_number,
|
|
inv.booking_id, i.company_id, i.source_id, w.facility_id, inv.warehouse_id, inv.yard_id, inv.zone_id,
|
|
i.type, i.status::text, i.subtotal_amount, i.tax_amount, i.total_amount, i.paid_amount, i.balance_amount,
|
|
i.currency, inv.arrived_at, i.issued_at, i.issued_at, i.due_at, i.paid_at,
|
|
CASE WHEN i.status::text = 'CANCELLED' THEN i.updated_at ELSE NULL END,
|
|
i.payments, NULL
|
|
FROM freight.invoices i
|
|
LEFT JOIN freight.warehouse_inventory inv ON inv.id = i.source_id
|
|
LEFT JOIN freight.warehouses w ON w.id = inv.warehouse_id
|
|
WHERE i.source = 'warehouse'
|
|
ON CONFLICT (id) DO NOTHING;
|
|
`);
|
|
await queryRunner.query(`
|
|
INSERT INTO freight.warehouse_fee_invoice_items (
|
|
id, created_at, updated_at, deleted_at, invoice_id, fee_rule_id, fee_type,
|
|
description, quantity, unit_rate, amount, currency, chargeable_days, free_days
|
|
)
|
|
SELECT
|
|
l.id, l.created_at, l.updated_at, l.deleted_at, l.invoice_id,
|
|
NULLIF(l.metadata->>'feeRuleId', '')::uuid,
|
|
l.charge_type,
|
|
COALESCE(l.description, ''),
|
|
l.quantity, l.unit_rate, l.amount, l.currency,
|
|
NULLIF(l.metadata->>'chargeableDays', '')::int,
|
|
NULLIF(l.metadata->>'freeDays', '')::int
|
|
FROM freight.invoice_lines l
|
|
JOIN freight.invoices i ON i.id = l.invoice_id AND i.source = 'warehouse'
|
|
ON CONFLICT (id) DO NOTHING;
|
|
`);
|
|
|
|
// Remove the migrated rows from the central tables.
|
|
await queryRunner.query(`
|
|
DELETE FROM freight.invoice_lines
|
|
WHERE invoice_id IN (SELECT id FROM freight.invoices WHERE source = 'warehouse');
|
|
`);
|
|
await queryRunner.query(`DELETE FROM freight.invoices WHERE source = 'warehouse';`);
|
|
}
|
|
}
|