mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 22:58:17 +00:00
feat: empty container Import
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Empty container import is base rail freight for equipment carrying no cargo,
|
||||
* so it is sold per lane exactly like laden container freight.
|
||||
*
|
||||
* CK_rates_yard_scope gains EMPTY_CONTAINER in its yard-carrying branch: an
|
||||
* empty rate prices a leg (Djibouti -> Modjo), so both yards stay required.
|
||||
* Drop-and-recreate is the established shape for this constraint — see
|
||||
* 3430000000000-FuelSurcharge and 3640000000000-EthiopianCustomsClearance.
|
||||
*/
|
||||
export class EmptyContainerRateScope3890000000000 implements MigrationInterface {
|
||||
name = 'EmptyContainerRateScope3890000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_yard_scope"`,
|
||||
);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.rates ADD CONSTRAINT "CK_rates_yard_scope" CHECK (
|
||||
deleted_at IS NOT NULL OR status = 'SUPERSEDED' OR
|
||||
CASE
|
||||
WHEN (trigger = 'ALWAYS' AND applies_to IN ('BULK', 'CONTAINER', 'EMPTY_CONTAINER', 'INTERCITY'))
|
||||
OR trigger IN ('CUSTOMS_CLEARANCE', 'ETHIOPIAN_CUSTOMS_CLEARANCE', 'WITH_RETURN', 'FUEL')
|
||||
THEN origin_yard_id IS NOT NULL AND destination_yard_id IS NOT NULL
|
||||
ELSE origin_yard_id IS NULL AND destination_yard_id IS NULL
|
||||
END
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_yard_scope"`,
|
||||
);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.rates ADD CONSTRAINT "CK_rates_yard_scope" CHECK (
|
||||
deleted_at IS NOT NULL OR status = 'SUPERSEDED' OR
|
||||
CASE
|
||||
WHEN (trigger = 'ALWAYS' AND applies_to IN ('BULK', 'CONTAINER', 'INTERCITY'))
|
||||
OR trigger IN ('CUSTOMS_CLEARANCE', 'ETHIOPIAN_CUSTOMS_CLEARANCE', 'WITH_RETURN', 'FUEL')
|
||||
THEN origin_yard_id IS NOT NULL AND destination_yard_id IS NOT NULL
|
||||
ELSE origin_yard_id IS NULL AND destination_yard_id IS NULL
|
||||
END
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Whether a booking moves cargo or bare equipment.
|
||||
*
|
||||
* EMPTY is container freight carrying nothing — the box itself is the shipment,
|
||||
* priced per size and lane off an EMPTY_CONTAINER_IMPORT rate. Deliberately a
|
||||
* separate column rather than a third `freight_type`: an empty booking is still
|
||||
* CONTAINER freight for wagon footprint, yard and warehouse allocation, train
|
||||
* scheduling, marshalling and gate passes, and `freight_type` is read in ~880
|
||||
* places whose else-arm means "container".
|
||||
*
|
||||
* Every existing row is LADEN, which the default supplies — no backfill needed.
|
||||
*/
|
||||
export class BookingCargoCondition3900000000000 implements MigrationInterface {
|
||||
name = 'BookingCargoCondition3900000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
ADD COLUMN IF NOT EXISTS cargo_condition varchar(10) NOT NULL DEFAULT 'LADEN'
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP CONSTRAINT IF EXISTS "CK_bookings_cargo_condition"
|
||||
`);
|
||||
// Bulk carries no equipment of its own, so EMPTY only ever rides CONTAINER
|
||||
// freight. Enforced here so no API path can file the combination.
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
ADD CONSTRAINT "CK_bookings_cargo_condition" CHECK (
|
||||
cargo_condition IN ('LADEN', 'EMPTY')
|
||||
AND (cargo_condition = 'LADEN' OR freight_type = 'CONTAINER')
|
||||
)
|
||||
`);
|
||||
|
||||
// The booking queues filter empties out of (and into) the laden lists.
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_cargo_condition
|
||||
ON freight.bookings (cargo_condition)
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX IF EXISTS freight.idx_bookings_cargo_condition`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings DROP CONSTRAINT IF EXISTS "CK_bookings_cargo_condition"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS cargo_condition`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
import { CONTRACT_TEMPLATE_DEFAULTS } from '../seed/data/contract-template-defaults';
|
||||
|
||||
/**
|
||||
* Contract paper for empty container import.
|
||||
*
|
||||
* - contracts.cargo_condition mirrors bookings.cargo_condition, so a general
|
||||
* contract can commit to moving bare equipment.
|
||||
* - Seeds IMPORT_EMPTY_CONTAINER, the system template the document renderer
|
||||
* resolves for those contracts. It carries no customs variant: an empty box
|
||||
* has no declaration to clear, the same reason intercity is unsuffixed.
|
||||
*/
|
||||
const SEEDED_CODES = ['IMPORT_EMPTY_CONTAINER'] as const;
|
||||
|
||||
export class EmptyContainerContractTemplate3910000000000 implements MigrationInterface {
|
||||
name = 'EmptyContainerContractTemplate3910000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.contracts
|
||||
ADD COLUMN IF NOT EXISTS cargo_condition varchar(10) NOT NULL DEFAULT 'LADEN'
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.contracts
|
||||
DROP CONSTRAINT IF EXISTS "CK_contracts_cargo_condition"
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.contracts
|
||||
ADD CONSTRAINT "CK_contracts_cargo_condition" CHECK (
|
||||
cargo_condition IN ('LADEN', 'EMPTY')
|
||||
AND (cargo_condition = 'LADEN' OR freight_type = 'CONTAINER')
|
||||
)
|
||||
`);
|
||||
|
||||
for (const code of SEEDED_CODES) {
|
||||
const seed = CONTRACT_TEMPLATE_DEFAULTS.find((t) => t.code === code);
|
||||
if (!seed) throw new Error(`Missing contract template default for ${code}`);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO freight.contract_templates
|
||||
(id, code, name, description, document_title, whereas_clauses, articles,
|
||||
is_active, is_system, created_at, updated_at)
|
||||
SELECT gen_random_uuid(), $1::varchar, $2, $3, $4, $5::jsonb, $6::jsonb,
|
||||
true, true, now(), now()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.contract_templates
|
||||
WHERE code = $1::varchar AND deleted_at IS NULL
|
||||
)`,
|
||||
[
|
||||
seed.code,
|
||||
seed.name,
|
||||
seed.description,
|
||||
seed.documentTitle,
|
||||
JSON.stringify(seed.whereasClauses),
|
||||
JSON.stringify(
|
||||
seed.articles.map((article, index) => ({ ...article, order: index + 1 })),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DELETE FROM freight.contract_templates WHERE code = ANY($1::varchar[]) AND is_system = true`,
|
||||
[[...SEEDED_CODES]],
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.contracts DROP CONSTRAINT IF EXISTS "CK_contracts_cargo_condition"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.contracts DROP COLUMN IF EXISTS cargo_condition`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user