Merge pull request #782 from Tria-plc/freight_feature/usermanagement

add lashing surcharge for cargo types with hasLashing flag
This commit is contained in:
marshal
2026-07-18 02:27:50 +03:00
committed by GitHub
59 changed files with 1919 additions and 140 deletions

View File

@@ -0,0 +1,52 @@
import { MigrationInterface, QueryRunner } from "typeorm";
/**
* Add a global "booking close offset" — how long BEFORE departure a schedule's
* booking window shuts — configurable separately for import and export.
*
* When an offset is set, the window's close instant is `departure offset`
* (e.g. departure 17:00 with a 3-hour import offset closes at 14:00; departure
* Jul-10 16:00 with a 1-day export offset closes Jul-9 16:00). It caps the whole
* booking lifecycle: the first window close, every reopen cycle, and the export
* FCFS close all land at/at-or-before this cutoff instead of at departure.
*
* NULL / 0 preserves the previous behaviour exactly (import closes at
* open+duration clamped to departure; export closes at departure), so existing
* installs are unaffected until an offset is entered.
*
* `*_close_offset_minutes` on the global-rules singleton is the live config; the
* matching `rule_*_close_offset_minutes` snapshot on each schedule freezes it at
* creation so the batch board keeps drawing the window the customer was shown
* even after a later global-rules edit. Both are nullable with no backfill —
* absent means "no offset", the safe default.
*/
export class AddBookingCloseOffset2330000000000 implements MigrationInterface {
name = "AddBookingCloseOffset2330000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_scheduling_global_rules
ADD COLUMN IF NOT EXISTS import_close_offset_minutes integer,
ADD COLUMN IF NOT EXISTS export_close_offset_minutes integer;
`);
await queryRunner.query(`
ALTER TABLE freight.train_schedules
ADD COLUMN IF NOT EXISTS rule_import_close_offset_minutes integer,
ADD COLUMN IF NOT EXISTS rule_export_close_offset_minutes integer;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_schedules
DROP COLUMN IF EXISTS rule_import_close_offset_minutes,
DROP COLUMN IF EXISTS rule_export_close_offset_minutes;
`);
await queryRunner.query(`
ALTER TABLE freight.train_scheduling_global_rules
DROP COLUMN IF EXISTS import_close_offset_minutes,
DROP COLUMN IF EXISTS export_close_offset_minutes;
`);
}
}

View File

@@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner } from "typeorm";
/**
* Add `has_lashing` to cargo types.
*
* When true, every booking of that cargo type incurs the flat LASHING
* surcharge (a rate with trigger = 'LASHING'). Defaults to false so existing
* cargo ships without the fee until the flag is turned on.
*/
export class AddCargoTypeHasLashing2340000000000 implements MigrationInterface {
name = "AddCargoTypeHasLashing2340000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.cargo_types
ADD COLUMN IF NOT EXISTS has_lashing boolean NOT NULL DEFAULT false;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.cargo_types
DROP COLUMN IF EXISTS has_lashing;
`);
}
}

View File

@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from "typeorm";
/**
* Add an opt-in "reverse wagon order" flag to a train schedule.
*
* When true, the built wagon plan is flipped at build time so the physically-last
* wagon sits at position 1. Only the order (sequence_no) changes — composition and
* booking allocations travel with their slot. The flag is frozen on the schedule
* at creation and re-applied every time the wagon plan is rebuilt, so the stored
* train order and the schedule order always match.
*
* Defaults to false; existing schedules keep their as-built order.
*/
export class AddReverseWagonOrder2340000000000 implements MigrationInterface {
name = "AddReverseWagonOrder2340000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_schedules
ADD COLUMN IF NOT EXISTS reverse_wagon_order boolean NOT NULL DEFAULT false;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_schedules
DROP COLUMN IF EXISTS reverse_wagon_order;
`);
}
}

View File

@@ -0,0 +1,63 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { CONTRACT_TEMPLATE_DEFAULTS } from '../seed/data/contract-template-defaults';
/**
* Refresh the "pricing" article of each seeded contract template so it points
* at the live Rate Schedule instead of hardcoded price figures (USD 400/wagon,
* USD 919/40ft, …). The original CreateContractTemplates migration seeded the
* old prose with ON CONFLICT DO NOTHING, so those figures are frozen in the DB
* rows and would otherwise contradict the rate-config-driven schedule table now
* rendered under the pricing article.
*
* Only the article whose id = 'pricing' is touched, and only when its body
* still matches the originally-seeded prose — so any admin edit to the pricing
* article is left untouched. Idempotent: re-running is a no-op once refreshed.
*/
export class RefreshContractPricingArticles2350000000000
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
for (const seed of CONTRACT_TEMPLATE_DEFAULTS) {
const pricing = seed.articles.find((article) => article.id === 'pricing');
if (!pricing) continue;
// jsonb_set the title + body of the element whose id = 'pricing', matched
// by array index. Guarded so admin-edited bodies are never overwritten.
await queryRunner.query(
`
UPDATE freight.contract_templates ct
SET articles = (
SELECT jsonb_agg(
CASE
WHEN elem->>'id' = 'pricing'
THEN elem || jsonb_build_object('title', $2::text, 'body', $3::text)
ELSE elem
END
)
FROM jsonb_array_elements(ct.articles) elem
)
WHERE ct.code = $1
AND EXISTS (
SELECT 1 FROM jsonb_array_elements(ct.articles) e
WHERE e->>'id' = 'pricing'
AND e->>'body' LIKE ANY (ARRAY[
'%USD 59.4 per metric ton%',
'%USD 696 (six hundred ninety-six) per wagon%',
'%USD 400 (four hundred) per wagon%',
'%From SGTD to Dire Dawa dry port, the rate is USD 919%',
'%Railway transportation charges from GMP to SGTD: USD 819%',
'%prevailing EDR domestic container tariff, as set out in the commercial schedule%'
])
);
`,
[seed.code, pricing.title, pricing.body],
);
}
}
public async down(): Promise<void> {
// No-op: the refreshed pricing prose is the correct forward state; reverting
// to hardcoded figures would reintroduce the rate-schedule contradiction.
}
}