feat(rates): last-mile rate rules with bulk per-ton-km and container distance bands

- rates: min_km/max_km columns, PER_TON_KM unit, ETB|USD currency for last mile
- extend UQ_rates_pattern with band start; overlap + shape validation
- shared last-mile charge resolver; approve-dialog price estimate endpoint
- delivery-fee invoice prices via rules, falls back to vehicle price/km
- backoffice: last-mile rate form (mode, container type, band, currency)
This commit is contained in:
Hagernesh
2026-08-05 22:12:40 +00:00
parent 6125f644b1
commit 5c8c68e990
25 changed files with 880 additions and 20 deletions

View File

@@ -0,0 +1,68 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Last-mile rate bands: adds min_km/max_km to freight.rates so container
* last-mile rates can be one row per (container size × distance band),
* and extends UQ_rates_pattern with the band start so sibling bands don't
* collide. Existing rows all have NULL min_km (COALESCE → -1), so the
* uniqueness semantics for every current rate are unchanged.
*/
export class LastMileRateBands3280000000000 implements MigrationInterface {
name = 'LastMileRateBands3280000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.rates ADD COLUMN IF NOT EXISTS min_km numeric(10,2)
`);
await queryRunner.query(`
ALTER TABLE freight.rates ADD COLUMN IF NOT EXISTS max_km numeric(10,2)
`);
await queryRunner.query(`
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'CK_rates_km_band'
AND conrelid = 'freight.rates'::regclass
) THEN
ALTER TABLE freight.rates ADD CONSTRAINT "CK_rates_km_band"
CHECK (max_km IS NULL OR (min_km IS NOT NULL AND max_km > min_km));
END IF;
END $$
`);
await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_rates_pattern"`);
await queryRunner.query(`
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_rates_pattern" ON freight.rates USING btree (
rate_type,
COALESCE(container_type_id, '00000000-0000-0000-0000-000000000000'::uuid),
COALESCE(cargo_type_id, '00000000-0000-0000-0000-000000000000'::uuid),
COALESCE(trade_direction, ''::character varying),
COALESCE(origin_yard_id, '00000000-0000-0000-0000-000000000000'::uuid),
COALESCE(destination_yard_id, '00000000-0000-0000-0000-000000000000'::uuid),
rate_unit,
COALESCE(min_km, '-1'::numeric)
) WHERE ((deleted_at IS NULL) AND ((status)::text <> 'SUPERSEDED'::text))
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_rates_pattern"`);
await queryRunner.query(`
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_rates_pattern" ON freight.rates USING btree (
rate_type,
COALESCE(container_type_id, '00000000-0000-0000-0000-000000000000'::uuid),
COALESCE(cargo_type_id, '00000000-0000-0000-0000-000000000000'::uuid),
COALESCE(trade_direction, ''::character varying),
COALESCE(origin_yard_id, '00000000-0000-0000-0000-000000000000'::uuid),
COALESCE(destination_yard_id, '00000000-0000-0000-0000-000000000000'::uuid),
rate_unit
) WHERE ((deleted_at IS NULL) AND ((status)::text <> 'SUPERSEDED'::text))
`);
await queryRunner.query(`
ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_km_band"
`);
await queryRunner.query(`ALTER TABLE freight.rates DROP COLUMN IF EXISTS max_km`);
await queryRunner.query(`ALTER TABLE freight.rates DROP COLUMN IF EXISTS min_km`);
}
}