This commit is contained in:
marshal
2026-07-02 13:02:33 +03:00
256 changed files with 15730 additions and 5422 deletions

View File

@@ -0,0 +1,78 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
/**
* Create the freight.last_mile_container_allocations table — container allocation
* records linking last-mile deliveries with containers and vehicles.
*/
export class CreateLastMileContainerAllocations1810000000002 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.last_mile_container_allocations');
if (exists) return;
await queryRunner.createTable(
new Table({
name: 'freight.last_mile_container_allocations',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'gen_random_uuid()',
},
{ name: 'last_mile_id', type: 'uuid', isNullable: false },
{ name: 'container_id', type: 'uuid', isNullable: false },
{ name: 'vehicle_id', type: 'uuid', isNullable: true },
{
name: 'container_type',
type: 'text',
isNullable: false,
},
{
name: 'quantity',
type: 'integer',
default: 1,
isNullable: false,
},
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createForeignKey(
'freight.last_mile_container_allocations',
new TableForeignKey({
columnNames: ['last_mile_id'],
referencedTableName: 'freight.last_mile',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'freight.last_mile_container_allocations',
new TableForeignKey({
columnNames: ['vehicle_id'],
referencedTableName: 'freight.vehicles',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.query(
`CREATE INDEX "IDX_last_mile_container_allocations_last_mile_id" ON "freight"."last_mile_container_allocations" ("last_mile_id")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_last_mile_container_allocations_vehicle_id" ON "freight"."last_mile_container_allocations" ("vehicle_id")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.last_mile_container_allocations');
if (exists) {
await queryRunner.dropTable('freight.last_mile_container_allocations');
}
}
}

View File

@@ -15,19 +15,25 @@ 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'
);
`);
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 freight.invoices (
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,
@@ -96,6 +102,8 @@ export class CreateInvoices1821000000002 implements MigrationInterface {
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;`);
await queryRunner.query(
`DROP TYPE IF EXISTS freight.invoices_status_enum;`,
);
}
}

View File

@@ -0,0 +1,80 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
/**
* Create the freight.booking_container_allocations table — container-to-vehicle
* allocation mapping for flexible routing of containers across available vehicles.
*/
export class CreateBookingContainerAllocations1825000000000 implements MigrationInterface {
name = 'CreateBookingContainerAllocations1825000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.booking_container_allocations');
if (exists) return;
await queryRunner.createTable(
new Table({
name: 'freight.booking_container_allocations',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'gen_random_uuid()',
},
{ name: 'booking_id', type: 'uuid', isNullable: false },
{ name: 'container_id', type: 'uuid', isNullable: false },
{ name: 'vehicle_id', type: 'uuid', isNullable: true },
{
name: 'container_type',
type: 'text',
isNullable: false,
},
{
name: 'quantity',
type: 'integer',
default: 1,
isNullable: false,
},
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createForeignKey(
'freight.booking_container_allocations',
new TableForeignKey({
columnNames: ['booking_id'],
referencedTableName: 'freight.bookings',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'freight.booking_container_allocations',
new TableForeignKey({
columnNames: ['vehicle_id'],
referencedTableName: 'freight.vehicles',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.query(
`CREATE INDEX "IDX_booking_container_allocations_booking_id" ON "freight"."booking_container_allocations" ("booking_id")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_booking_container_allocations_vehicle_id" ON "freight"."booking_container_allocations" ("vehicle_id")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.booking_container_allocations');
if (exists) {
await queryRunner.dropTable('freight.booking_container_allocations');
}
}
}

View File

@@ -0,0 +1,34 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddGrnNumberToWarehouseInventory1828000000000 implements MigrationInterface {
name = 'AddGrnNumberToWarehouseInventory1828000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.warehouse_inventory
ADD COLUMN IF NOT EXISTS grn_number VARCHAR(100) NULL
`);
await queryRunner.query(`
UPDATE freight.warehouse_inventory
SET grn_number = substring(notes FROM 'GRN Number: ([^\\n\\r]+)')
WHERE grn_number IS NULL
AND notes IS NOT NULL
AND notes ~ 'GRN Number: '
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_warehouse_inventory_grn_number
ON freight.warehouse_inventory(grn_number)
WHERE grn_number IS NOT NULL
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_warehouse_inventory_grn_number`);
await queryRunner.query(`
ALTER TABLE freight.warehouse_inventory
DROP COLUMN IF EXISTS grn_number
`);
}
}

View File

@@ -0,0 +1,71 @@
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<void> {
// 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<void> {
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).
}
}

View File

@@ -0,0 +1,222 @@
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> {
// 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';`);
}
}

View File

@@ -0,0 +1,27 @@
import { MigrationInterface, QueryRunner } from "typeorm";
/**
* Add the `EXPIRED` invoice status. An invoice expires when its source's pay
* window closes before settlement (e.g. a booking whose `paymentDeadline`
* lapses) — driven event-style from the domain via `BillingService.expirePayable`,
* which emits `${source}.invoice.expired`. Terminal and not settle-able (kept out
* of `OPEN_STATUSES`), so it is distinct from `CANCELLED` (manual void) and
* `OVERDUE` (still payable).
*
* Matches Freight.InvoiceStatus in packages/types. ADD VALUE only — additive and
* not referenced in this same transaction, so it is PG 12+ safe.
*/
export class AddExpiredInvoiceStatus1830000000000 implements MigrationInterface {
name = "AddExpiredInvoiceStatus1830000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TYPE freight.invoices_status_enum ADD VALUE IF NOT EXISTS 'EXPIRED' AFTER 'REFUNDED';`,
);
}
public async down(): Promise<void> {
// Postgres cannot drop individual enum values; EXPIRED is left on
// freight.invoices_status_enum (harmless, unused after down).
}
}

View File

@@ -0,0 +1,74 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
/**
* Create freight.first_mile_container_allocations table — tracks
* container allocations per first-mile shipment with optional vehicle assignment.
*/
export class CreateFirstMileContainerAllocations1830000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.first_mile_container_allocations');
if (exists) return;
await queryRunner.createTable(
new Table({
name: 'freight.first_mile_container_allocations',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'gen_random_uuid()',
},
{ name: 'first_mile_id', type: 'uuid', isNullable: false },
{ name: 'container_id', type: 'uuid', isNullable: false },
{ name: 'vehicle_id', type: 'uuid', isNullable: true },
{ name: 'container_type', type: 'text', isNullable: false },
{
name: 'quantity',
type: 'int',
default: 1,
isNullable: false,
},
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createForeignKey(
'freight.first_mile_container_allocations',
new TableForeignKey({
columnNames: ['first_mile_id'],
referencedTableName: 'freight.first_mile',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'freight.first_mile_container_allocations',
new TableForeignKey({
columnNames: ['vehicle_id'],
referencedTableName: 'freight.vehicles',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.query(
`CREATE INDEX "IDX_first_mile_container_allocations_first_mile_id" ON "freight"."first_mile_container_allocations" ("first_mile_id")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_first_mile_container_allocations_vehicle_id" ON "freight"."first_mile_container_allocations" ("vehicle_id")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.first_mile_container_allocations');
if (exists) {
await queryRunner.dropTable('freight.first_mile_container_allocations');
}
}
}

View File

@@ -0,0 +1,79 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateFuelTables1840000000000 implements MigrationInterface {
name = "CreateFuelTables1840000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
const fuelPurchasesExists = await queryRunner.query(
`SELECT 1 FROM information_schema.tables WHERE table_schema = 'freight' AND table_name = 'fuel_purchases';`,
);
if (!fuelPurchasesExists.length) {
await queryRunner.query(`
CREATE TABLE freight.fuel_purchases (
id uuid NOT NULL DEFAULT gen_random_uuid(),
vehicle_id uuid NOT NULL,
purchase_date timestamptz NOT NULL,
liters numeric(10, 2) NOT NULL,
cost_per_liter numeric(10, 2) NOT NULL,
total_cost numeric(14, 2) NOT NULL,
fuel_station varchar(255) NULL,
payment_method varchar(50) DEFAULT 'CASH',
odometer_reading numeric(10, 2) NULL,
driver_id uuid NULL,
receipt_number varchar(255) NULL,
notes text NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz NULL,
CONSTRAINT pk_fuel_purchases PRIMARY KEY (id),
CONSTRAINT fk_fuel_purchases_vehicle FOREIGN KEY (vehicle_id)
REFERENCES freight.vehicles (id) ON DELETE CASCADE
);
`);
await queryRunner.query(
`CREATE INDEX idx_fuel_purchases_vehicle ON freight.fuel_purchases (vehicle_id);`,
);
await queryRunner.query(
`CREATE INDEX idx_fuel_purchases_date ON freight.fuel_purchases (purchase_date);`,
);
}
const fuelConsumptionExists = await queryRunner.query(
`SELECT 1 FROM information_schema.tables WHERE table_schema = 'freight' AND table_name = 'fuel_consumption';`,
);
if (!fuelConsumptionExists.length) {
await queryRunner.query(`
CREATE TABLE freight.fuel_consumption (
id uuid NOT NULL DEFAULT gen_random_uuid(),
vehicle_id uuid NOT NULL,
month date NOT NULL,
total_liters numeric(10, 2) NOT NULL,
total_cost numeric(14, 2) NOT NULL,
total_distance_km numeric(10, 2) NOT NULL,
fuel_efficiency_km_per_l numeric(10, 2) NULL,
number_of_purchases integer DEFAULT 0,
average_cost_per_liter numeric(10, 2) NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz NULL,
CONSTRAINT pk_fuel_consumption PRIMARY KEY (id),
CONSTRAINT fk_fuel_consumption_vehicle FOREIGN KEY (vehicle_id)
REFERENCES freight.vehicles (id) ON DELETE CASCADE,
CONSTRAINT uq_fuel_consumption_vehicle_month UNIQUE (vehicle_id, month)
);
`);
await queryRunner.query(
`CREATE INDEX idx_fuel_consumption_vehicle_month ON freight.fuel_consumption (vehicle_id, month);`,
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.fuel_consumption;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.fuel_purchases;`);
}
}

View File

@@ -0,0 +1,82 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateMaintenanceTables1850000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Create maintenance_schedules table
const scheduleTableExists = await queryRunner.query(`
SELECT EXISTS(
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'freight' AND table_name = 'maintenance_schedules'
)
`);
if (!scheduleTableExists[0].exists) {
await queryRunner.query(`
CREATE TABLE "freight"."maintenance_schedules" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"vehicle_id" uuid NOT NULL,
"maintenance_type" varchar NOT NULL,
"description" varchar NOT NULL,
"scheduled_date" timestamptz NOT NULL,
"completed_date" timestamptz,
"estimated_cost" numeric(14,2),
"actual_cost" numeric(14,2),
"status" varchar NOT NULL DEFAULT 'SCHEDULED',
"odometer_reading" numeric,
"service_provider" varchar,
"notes" text,
"next_due_km" numeric,
"next_due_date" timestamptz,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now(),
"deleted_at" timestamptz,
PRIMARY KEY ("id")
)
`);
await queryRunner.query(
`CREATE INDEX "idx_maintenance_schedules_vehicle_date" ON "freight"."maintenance_schedules" ("vehicle_id", "scheduled_date")`
);
}
// Create maintenance_costs table
const costsTableExists = await queryRunner.query(`
SELECT EXISTS(
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'freight' AND table_name = 'maintenance_costs'
)
`);
if (!costsTableExists[0].exists) {
await queryRunner.query(`
CREATE TABLE "freight"."maintenance_costs" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"vehicle_id" uuid NOT NULL,
"maintenance_schedule_id" uuid,
"incurred_date" timestamptz NOT NULL,
"cost_amount" numeric(14,2) NOT NULL,
"cost_type" varchar NOT NULL,
"description" varchar NOT NULL,
"service_provider" varchar,
"invoice_number" varchar,
"notes" text,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now(),
"deleted_at" timestamptz,
PRIMARY KEY ("id"),
CONSTRAINT "fk_maintenance_schedule" FOREIGN KEY ("maintenance_schedule_id")
REFERENCES "freight"."maintenance_schedules" ("id") ON DELETE SET NULL
)
`);
await queryRunner.query(
`CREATE INDEX "idx_maintenance_costs_vehicle_date" ON "freight"."maintenance_costs" ("vehicle_id", "incurred_date")`
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "freight"."maintenance_costs"`);
await queryRunner.query(`DROP TABLE IF EXISTS "freight"."maintenance_schedules"`);
}
}

View File

@@ -0,0 +1,34 @@
import { MigrationInterface, QueryRunner } from "typeorm";
/**
* Add paid column to first_mile and last_mile tables to track invoice payment status.
*/
export class AddPaidToFirstAndLastMile1860000000000
implements MigrationInterface
{
name = "AddPaidToFirstAndLastMile1860000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.first_mile
ADD COLUMN IF NOT EXISTS paid boolean NOT NULL DEFAULT false;
`);
await queryRunner.query(`
ALTER TABLE freight.last_mile
ADD COLUMN IF NOT EXISTS paid boolean NOT NULL DEFAULT false;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.first_mile
DROP COLUMN IF EXISTS paid;
`);
await queryRunner.query(`
ALTER TABLE freight.last_mile
DROP COLUMN IF EXISTS paid;
`);
}
}