mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 10:45:44 +00:00
fix issue
This commit is contained in:
74
e2e/freight/cypress/fixtures/seed-flow2-export-legs.sql
Normal file
74
e2e/freight/cypress/fixtures/seed-flow2-export-legs.sql
Normal file
@@ -0,0 +1,74 @@
|
||||
-- FLOW-TWO fixture — rates for the REVERSED (export-direction) corridor.
|
||||
--
|
||||
-- Run AFTER seed-import-corridor.sql and seed-flow2-legs.sql. Idempotent.
|
||||
--
|
||||
-- seed-flow2-legs.sql rates every FORWARD pair (i < j, DJIB_PORT-first order).
|
||||
-- The export scenarios (TC-01 … TC-20 of the second flow-two batch) run the
|
||||
-- corridor the other way — F→A — and book MID-corridor export legs like
|
||||
-- MOJO→DJIB_PORT and DIRE_DAWA→NAGAD. Pricing hard-blocks a booking on a pair
|
||||
-- with no LIVE rate, so every REVERSED ordered pair gets one here.
|
||||
--
|
||||
-- Corridor stop order (CORRIDOR in import-utils.ts):
|
||||
-- A=DJIB_PORT B=NAGAD C=DIRE_DAWA D=E2E_AWASH E=MOJO F=KALITY
|
||||
-- Export runs F→A, so an export leg is (higher seq) → (lower seq).
|
||||
--
|
||||
-- DIRECTION IS DERIVED FROM THE YARDS' COUNTRIES, never from our intent
|
||||
-- (resolveTradeDirectionForBooking). So:
|
||||
-- x → DJIB_PORT crosses the border → EXPORT → CONTAINER_EXPORT / BULK_EXPORT
|
||||
-- wholly-Ethiopian reversed pair → DOMESTIC → INTERCITY_CONTAINER / _BULK
|
||||
-- Getting the rate_type wrong does not fail loudly — the booking 404s at
|
||||
-- pricing, far from the cause.
|
||||
--
|
||||
-- Flat, distance-free values: these specs assert capacity, never money.
|
||||
|
||||
DROP TABLE IF EXISTS flow2_rev_pairs;
|
||||
CREATE TEMP TABLE flow2_rev_pairs AS
|
||||
WITH stops(code, seq) AS (
|
||||
VALUES ('DJIB_PORT', 1), ('NAGAD', 2), ('DIRE_DAWA', 3),
|
||||
('E2E_AWASH', 4), ('MOJO', 5), ('KALITY', 6)
|
||||
)
|
||||
-- a.seq > b.seq — the reversed direction, i.e. running toward the port.
|
||||
SELECT a.code AS from_code, b.code AS to_code, (a.seq - b.seq) AS legs
|
||||
FROM stops a JOIN stops b ON a.seq > b.seq;
|
||||
|
||||
-- 1. Freight rates on every reversed pair.
|
||||
INSERT INTO freight.rates
|
||||
(id, rate_type, applies_to, trigger, currency, rate_value, rate_unit, status,
|
||||
origin_yard_id, destination_yard_id, proposed_by_staff_id)
|
||||
SELECT gen_random_uuid(), v.rate_type, v.applies_to, 'ALWAYS', 'USD',
|
||||
v.value, v.unit, 'LIVE', a.id, b.id, u.id
|
||||
FROM flow2_rev_pairs p
|
||||
CROSS JOIN LATERAL (VALUES
|
||||
(CASE WHEN p.to_code = 'DJIB_PORT' THEN 'CONTAINER_EXPORT'
|
||||
ELSE 'INTERCITY_CONTAINER' END,
|
||||
CASE WHEN p.to_code = 'DJIB_PORT' THEN 'CONTAINER' ELSE 'INTERCITY' END,
|
||||
100 * p.legs, 'PER_CONTAINER'),
|
||||
(CASE WHEN p.to_code = 'DJIB_PORT' THEN 'BULK_EXPORT'
|
||||
ELSE 'INTERCITY_BULK' END,
|
||||
CASE WHEN p.to_code = 'DJIB_PORT' THEN 'BULK' ELSE 'INTERCITY' END,
|
||||
5 * p.legs, 'PER_TON')
|
||||
) AS v(rate_type, applies_to, value, unit)
|
||||
JOIN freight.yards a ON a.code = p.from_code
|
||||
JOIN freight.yards b ON b.code = p.to_code
|
||||
JOIN iam.users u ON u.email = 'operation@edr.local'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.rates r
|
||||
WHERE r.rate_type = v.rate_type
|
||||
AND r.origin_yard_id = a.id AND r.destination_yard_id = b.id
|
||||
AND r.deleted_at IS NULL
|
||||
);
|
||||
|
||||
-- 2. Yard distances on the reversed pairs. yard_distances is NOT symmetric —
|
||||
-- pricing and the available-days lookup both walk it directionally, and a
|
||||
-- missing row reads as "not on the network".
|
||||
INSERT INTO freight.yard_distances (id, from_yard_id, to_yard_id, distance_km)
|
||||
SELECT gen_random_uuid(), a.id, b.id, 100 * p.legs
|
||||
FROM flow2_rev_pairs p
|
||||
JOIN freight.yards a ON a.code = p.from_code
|
||||
JOIN freight.yards b ON b.code = p.to_code
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.yard_distances d
|
||||
WHERE d.from_yard_id = a.id AND d.to_yard_id = b.id AND d.deleted_at IS NULL
|
||||
);
|
||||
|
||||
DROP TABLE flow2_rev_pairs;
|
||||
112
e2e/freight/cypress/fixtures/seed-flow2-export-train-2.sql
Normal file
112
e2e/freight/cypress/fixtures/seed-flow2-export-train-2.sql
Normal file
@@ -0,0 +1,112 @@
|
||||
-- FLOW-TWO fixture — a SECOND export train, identical in shape to TRN-F2-EXP.
|
||||
--
|
||||
-- Run AFTER seed-import-corridor.sql and seed-flow2-export-train.sql.
|
||||
-- Idempotent.
|
||||
--
|
||||
-- The multi-train scenarios (TC-13, TC-14, TC-16) need two export trains on one
|
||||
-- day with the SAME pool structure, so that "which train did this booking land
|
||||
-- on" is the only variable. A second train with different pools would confound
|
||||
-- the overflow question with a capacity question.
|
||||
--
|
||||
-- 35 × NW5 (CNT) + 20 × CW4 (BLK) + 5 × NW6 (FLT) = 60, at KALITY
|
||||
--
|
||||
-- Its wagon numbers (WGN-F2Y-*) are distinct from TRN-F2-EXP's (WGN-F2X-*), so
|
||||
-- the two consists never compete for the same physical stock — a shared wagon
|
||||
-- pinned by the other train's schedule would make one consist silently short
|
||||
-- and the overflow arithmetic wrong.
|
||||
--
|
||||
-- Length and pull are slack for the same reason as the first train (862.3 m of
|
||||
-- wagons behind 1000 m locos, ~5700 T behind 12000 T of pull): this suite tests
|
||||
-- SLOTS and TYPE, and a length rejection would masquerade as a pool rejection.
|
||||
|
||||
-- 1. Its own locomotive pair at KALITY.
|
||||
INSERT INTO freight.locomotives
|
||||
(id, code, max_pull_weight_tons, max_train_length_meters,
|
||||
overage_tolerance_tons, current_yard_id)
|
||||
SELECT gen_random_uuid(), v.code, 12000, 1000, 0, y.id
|
||||
FROM (VALUES ('LOCO-F2Y-A'), ('LOCO-F2Y-B')) AS v(code)
|
||||
JOIN freight.yards y ON y.code = 'KALITY'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.locomotives l WHERE l.code = v.code);
|
||||
|
||||
UPDATE freight.locomotives
|
||||
SET max_pull_weight_tons = 12000, max_train_length_meters = 1000,
|
||||
overage_tolerance_tons = 0,
|
||||
current_yard_id = (SELECT id FROM freight.yards WHERE code = 'KALITY')
|
||||
WHERE code IN ('LOCO-F2Y-A', 'LOCO-F2Y-B')
|
||||
AND (max_pull_weight_tons IS DISTINCT FROM 12000
|
||||
OR max_train_length_meters IS DISTINCT FROM 1000
|
||||
OR overage_tolerance_tons IS DISTINCT FROM 0);
|
||||
|
||||
-- 2. The train.
|
||||
INSERT INTO freight.trains
|
||||
(id, code, train_name, capacity_tons, current_yard_id,
|
||||
import_train_number, export_train_number)
|
||||
SELECT gen_random_uuid(), 'TRN-F2-EXP2', 'E2E Flow-2 Export Three-Pool II', 4200, y.id,
|
||||
'9314', '9313'
|
||||
FROM freight.yards y
|
||||
WHERE y.code = 'KALITY'
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.trains t WHERE t.code = 'TRN-F2-EXP2');
|
||||
|
||||
INSERT INTO freight.train_locomotives (id, train_id, locomotive_id, sequence_no)
|
||||
SELECT gen_random_uuid(), t.id, l.id, v.seq
|
||||
FROM (VALUES ('LOCO-F2Y-A', 0), ('LOCO-F2Y-B', 1)) AS v(loco_code, seq)
|
||||
JOIN freight.trains t ON t.code = 'TRN-F2-EXP2'
|
||||
JOIN freight.locomotives l ON l.code = v.loco_code
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.train_locomotives tl
|
||||
WHERE tl.train_id = t.id AND tl.locomotive_id = l.id
|
||||
);
|
||||
|
||||
-- 3. Its own three pools of rolling stock at KALITY.
|
||||
INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, current_yard_id)
|
||||
SELECT gen_random_uuid(), 'WGN-F2Y-C' || lpad(g::text, 2, '0'), wt.id, y.id
|
||||
FROM generate_series(1, 35) AS g
|
||||
JOIN freight.wagon_types wt ON wt.code = 'NW5'
|
||||
JOIN freight.yards y ON y.code = 'KALITY'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.wagons w
|
||||
WHERE w.wagon_number = 'WGN-F2Y-C' || lpad(g::text, 2, '0')
|
||||
);
|
||||
|
||||
INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, current_yard_id)
|
||||
SELECT gen_random_uuid(), 'WGN-F2Y-B' || lpad(g::text, 2, '0'), wt.id, y.id
|
||||
FROM generate_series(1, 20) AS g
|
||||
JOIN freight.wagon_types wt ON wt.code = 'CW4'
|
||||
JOIN freight.yards y ON y.code = 'KALITY'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.wagons w
|
||||
WHERE w.wagon_number = 'WGN-F2Y-B' || lpad(g::text, 2, '0')
|
||||
);
|
||||
|
||||
INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, current_yard_id)
|
||||
SELECT gen_random_uuid(), 'WGN-F2Y-F' || lpad(g::text, 2, '0'), wt.id, y.id
|
||||
FROM generate_series(1, 5) AS g
|
||||
JOIN freight.wagon_types wt ON wt.code = 'NW6'
|
||||
JOIN freight.yards y ON y.code = 'KALITY'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.wagons w
|
||||
WHERE w.wagon_number = 'WGN-F2Y-F' || lpad(g::text, 2, '0')
|
||||
);
|
||||
|
||||
-- 4. Couple all 60, same order as the first train.
|
||||
UPDATE freight.wagons w
|
||||
SET train_id = t.id,
|
||||
sequence_number = g.seq,
|
||||
status = 'ASSIGNED',
|
||||
current_yard_id = t.current_yard_id
|
||||
FROM freight.trains t,
|
||||
LATERAL (
|
||||
SELECT ('WGN-F2Y-C' || lpad(s::text, 2, '0')) AS num, s AS seq
|
||||
FROM generate_series(1, 35) AS s
|
||||
UNION ALL
|
||||
SELECT ('WGN-F2Y-B' || lpad(s::text, 2, '0')), 35 + s
|
||||
FROM generate_series(1, 20) AS s
|
||||
UNION ALL
|
||||
SELECT ('WGN-F2Y-F' || lpad(s::text, 2, '0')), 55 + s
|
||||
FROM generate_series(1, 5) AS s
|
||||
) g
|
||||
WHERE t.code = 'TRN-F2-EXP2'
|
||||
AND w.wagon_number = g.num
|
||||
AND (w.train_id IS DISTINCT FROM t.id
|
||||
OR w.sequence_number IS DISTINCT FROM g.seq
|
||||
OR w.status IS DISTINCT FROM 'ASSIGNED');
|
||||
182
e2e/freight/cypress/fixtures/seed-flow2-export-train.sql
Normal file
182
e2e/freight/cypress/fixtures/seed-flow2-export-train.sql
Normal file
@@ -0,0 +1,182 @@
|
||||
-- FLOW-TWO fixture — the EXPORT-direction, THREE-POOL consist.
|
||||
--
|
||||
-- Run AFTER seed-import-corridor.sql. Idempotent.
|
||||
--
|
||||
-- WHY THIS TRAIN EXISTS
|
||||
--
|
||||
-- The second flow-two batch is written against a "60 wagons = 35 CNT + 20 BLK
|
||||
-- + 5 FLT" baseline, running F→A (KALITY → DJIB_PORT). Nothing in the existing
|
||||
-- fixtures gives that:
|
||||
-- - TRN-G1-1 is 53 × NW5, one pool, parked at DJIB_PORT (import end).
|
||||
-- - TRN-F2-MIX is 30 NW5 + 20 PW2 — two pools, also at DJIB_PORT.
|
||||
-- - TRN-LEDGER-PW2 is 37 PW2 at KALITY — right end, wrong pools.
|
||||
--
|
||||
-- TRN-F2-EXP is 60 wagons at KALITY split across THREE cargo-incompatible
|
||||
-- types, so "container overflow must not eat bulk wagons" (TC-02) and
|
||||
-- "bulk-to-container substitution" (TC-03) are expressible at all. On a
|
||||
-- one-pool consist the abstract slot count and the physical stock count are
|
||||
-- the same number, and a broken pool separation is invisible.
|
||||
--
|
||||
-- 35 × NW5 — CNT. The only type 20FT/40FT containers ride
|
||||
-- (container_type_wagon_types, seed-import-corridor 2b).
|
||||
-- 20 × CW4 — BLK. Carries E2E_IMP_WHEAT (cargo_type_wagon_types, 5b2).
|
||||
-- 5 × NW6 — FLT. Allow-listed to NOTHING here, deliberately: it is the
|
||||
-- idle-but-unusable pool TC-02 asserts a container booking may
|
||||
-- NOT reach for. See section 5.
|
||||
--
|
||||
-- LENGTH AND PULL MUST NOT BIND — this suite tests SLOTS and TYPE, and a
|
||||
-- length rejection would masquerade as a pool rejection:
|
||||
-- 35 × 14.000 + 20 × 13.976 + 5 × 18.560 = 490.0 + 279.5 + 92.8 = 862.3 m
|
||||
-- so the locos below run 1000 m. Gross weight at full load is roughly
|
||||
-- 60 × (70 + ~25) ≈ 5700 T, so the pull is set to 12000 T. Both axes stay
|
||||
-- slack; only wagon slots and wagon TYPE can ever bind.
|
||||
--
|
||||
-- The wagon numbers (WGN-F2X-*) are distinct from every other fixture's, so
|
||||
-- this consist never competes for stock with the import specs.
|
||||
|
||||
-- 1. Locomotive pair at KALITY (the export origin), 1000 m so length is slack.
|
||||
INSERT INTO freight.locomotives
|
||||
(id, code, max_pull_weight_tons, max_train_length_meters,
|
||||
overage_tolerance_tons, current_yard_id)
|
||||
SELECT gen_random_uuid(), v.code, 12000, 1000, 0, y.id
|
||||
FROM (VALUES ('LOCO-F2X-A'), ('LOCO-F2X-B')) AS v(code)
|
||||
JOIN freight.yards y ON y.code = 'KALITY'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.locomotives l WHERE l.code = v.code);
|
||||
|
||||
-- Re-assert unconditionally: a prior run (or another fixture) could have left
|
||||
-- these at different limits, and a 760 m loco silently re-caps the consist.
|
||||
UPDATE freight.locomotives
|
||||
SET max_pull_weight_tons = 12000, max_train_length_meters = 1000,
|
||||
overage_tolerance_tons = 0,
|
||||
current_yard_id = (SELECT id FROM freight.yards WHERE code = 'KALITY')
|
||||
WHERE code IN ('LOCO-F2X-A', 'LOCO-F2X-B')
|
||||
AND (max_pull_weight_tons IS DISTINCT FROM 12000
|
||||
OR max_train_length_meters IS DISTINCT FROM 1000
|
||||
OR overage_tolerance_tons IS DISTINCT FROM 0);
|
||||
|
||||
-- 2. The train, parked at KALITY — a built-train schedule requires the consist
|
||||
-- to already stand at the schedule's origin.
|
||||
INSERT INTO freight.trains
|
||||
(id, code, train_name, capacity_tons, current_yard_id,
|
||||
import_train_number, export_train_number)
|
||||
SELECT gen_random_uuid(), 'TRN-F2-EXP', 'E2E Flow-2 Export Three-Pool', 4200, y.id,
|
||||
'9312', '9311'
|
||||
FROM freight.yards y
|
||||
WHERE y.code = 'KALITY'
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.trains t WHERE t.code = 'TRN-F2-EXP');
|
||||
|
||||
INSERT INTO freight.train_locomotives (id, train_id, locomotive_id, sequence_no)
|
||||
SELECT gen_random_uuid(), t.id, l.id, v.seq
|
||||
FROM (VALUES ('LOCO-F2X-A', 0), ('LOCO-F2X-B', 1)) AS v(loco_code, seq)
|
||||
JOIN freight.trains t ON t.code = 'TRN-F2-EXP'
|
||||
JOIN freight.locomotives l ON l.code = v.loco_code
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.train_locomotives tl
|
||||
WHERE tl.train_id = t.id AND tl.locomotive_id = l.id
|
||||
);
|
||||
|
||||
-- 3. The three pools of rolling stock, minted fresh at KALITY.
|
||||
--
|
||||
-- Minted rather than borrowed: the boot fleet at KALITY is already spoken
|
||||
-- for by the export-bulk and ledger specs, and a shared wagon that another
|
||||
-- spec's schedule has pinned makes this consist silently short.
|
||||
|
||||
-- 3a. 35 × NW5 — the CONTAINER pool.
|
||||
INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, current_yard_id)
|
||||
SELECT gen_random_uuid(), 'WGN-F2X-C' || lpad(g::text, 2, '0'), wt.id, y.id
|
||||
FROM generate_series(1, 35) AS g
|
||||
JOIN freight.wagon_types wt ON wt.code = 'NW5'
|
||||
JOIN freight.yards y ON y.code = 'KALITY'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.wagons w
|
||||
WHERE w.wagon_number = 'WGN-F2X-C' || lpad(g::text, 2, '0')
|
||||
);
|
||||
|
||||
-- 3b. 20 × CW4 — the BULK pool (E2E_IMP_WHEAT rides CW4).
|
||||
INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, current_yard_id)
|
||||
SELECT gen_random_uuid(), 'WGN-F2X-B' || lpad(g::text, 2, '0'), wt.id, y.id
|
||||
FROM generate_series(1, 20) AS g
|
||||
JOIN freight.wagon_types wt ON wt.code = 'CW4'
|
||||
JOIN freight.yards y ON y.code = 'KALITY'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.wagons w
|
||||
WHERE w.wagon_number = 'WGN-F2X-B' || lpad(g::text, 2, '0')
|
||||
);
|
||||
|
||||
-- 3c. 5 × NW6 — the FLATBED pool. Allow-listed to no cargo in this fixture.
|
||||
INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, current_yard_id)
|
||||
SELECT gen_random_uuid(), 'WGN-F2X-F' || lpad(g::text, 2, '0'), wt.id, y.id
|
||||
FROM generate_series(1, 5) AS g
|
||||
JOIN freight.wagon_types wt ON wt.code = 'NW6'
|
||||
JOIN freight.yards y ON y.code = 'KALITY'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.wagons w
|
||||
WHERE w.wagon_number = 'WGN-F2X-F' || lpad(g::text, 2, '0')
|
||||
);
|
||||
|
||||
-- 4. Couple all 60, CNT then BLK then FLT. Unconditionally re-asserted: a
|
||||
-- prior run could have left one detached, and a 59-wagon consist shifts
|
||||
-- every scenario's arithmetic by a slot.
|
||||
UPDATE freight.wagons w
|
||||
SET train_id = t.id,
|
||||
sequence_number = g.seq,
|
||||
status = 'ASSIGNED',
|
||||
current_yard_id = t.current_yard_id
|
||||
FROM freight.trains t,
|
||||
LATERAL (
|
||||
SELECT ('WGN-F2X-C' || lpad(s::text, 2, '0')) AS num, s AS seq
|
||||
FROM generate_series(1, 35) AS s
|
||||
UNION ALL
|
||||
SELECT ('WGN-F2X-B' || lpad(s::text, 2, '0')), 35 + s
|
||||
FROM generate_series(1, 20) AS s
|
||||
UNION ALL
|
||||
SELECT ('WGN-F2X-F' || lpad(s::text, 2, '0')), 55 + s
|
||||
FROM generate_series(1, 5) AS s
|
||||
) g
|
||||
WHERE t.code = 'TRN-F2-EXP'
|
||||
AND w.wagon_number = g.num
|
||||
AND (w.train_id IS DISTINCT FROM t.id
|
||||
OR w.sequence_number IS DISTINCT FROM g.seq
|
||||
OR w.status IS DISTINCT FROM 'ASSIGNED');
|
||||
|
||||
-- 5. The FLATBED pool's allow-list, asserted EMPTY.
|
||||
--
|
||||
-- TC-02 turns on NW6 being idle AND unreachable: a container booking that
|
||||
-- overflows the 35-wagon NW5 pool must be refused even though 5 NW6 slots
|
||||
-- stand free. If some other fixture ever allow-lists a container type or a
|
||||
-- cargo type onto NW6, that scenario silently starts passing for the wrong
|
||||
-- reason — so the link is removed here rather than merely never added.
|
||||
DELETE FROM freight.container_type_wagon_types x
|
||||
USING freight.wagon_types wt
|
||||
WHERE x.wagon_type_id = wt.id AND wt.code = 'NW6';
|
||||
|
||||
DELETE FROM freight.cargo_type_wagon_types x
|
||||
USING freight.cargo_types ct, freight.wagon_types wt
|
||||
WHERE x.cargo_type_id = ct.id AND x.wagon_type_id = wt.id
|
||||
AND wt.code = 'NW6'
|
||||
AND ct.code IN ('E2E_IMP_WHEAT', 'E2E_IMP_GRAINS', 'E2E_IMP_AUTO',
|
||||
'E2E_IMP_MACHINE', 'E2E_EXP_CEMENT', 'E2E_EXP_FERT');
|
||||
|
||||
-- 6. Segregation cargo types for TC-10 (bulk commodity segregation).
|
||||
--
|
||||
-- Fertilizer and cement are separate cargo TYPES sharing the CW4 pool with
|
||||
-- wheat. Whether the engine keeps them out of the same physical wagon is
|
||||
-- exactly what TC-10 asks — planWagonsWithStock tops off an existing wagon
|
||||
-- only when the cargo type matches, so distinct types is the mechanism.
|
||||
INSERT INTO freight.cargo_types (id, code, cargo_type_name, is_active)
|
||||
SELECT gen_random_uuid(), 'E2E_EXP_FERT', 'E2E Export Fertilizer', true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.cargo_types WHERE code = 'E2E_EXP_FERT');
|
||||
|
||||
INSERT INTO freight.cargo_types (id, code, cargo_type_name, is_active)
|
||||
SELECT gen_random_uuid(), 'E2E_EXP_CEMENT', 'E2E Export Cement', true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.cargo_types WHERE code = 'E2E_EXP_CEMENT');
|
||||
|
||||
INSERT INTO freight.cargo_type_wagon_types (cargo_type_id, wagon_type_id)
|
||||
SELECT ct.id, wt.id
|
||||
FROM freight.cargo_types ct
|
||||
JOIN freight.wagon_types wt ON wt.code = 'CW4'
|
||||
WHERE ct.code IN ('E2E_EXP_FERT', 'E2E_EXP_CEMENT')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM freight.cargo_type_wagon_types x
|
||||
WHERE x.cargo_type_id = ct.id AND x.wagon_type_id = wt.id
|
||||
);
|
||||
95
e2e/freight/cypress/fixtures/seed-flow2-legs.sql
Normal file
95
e2e/freight/cypress/fixtures/seed-flow2-legs.sql
Normal file
@@ -0,0 +1,95 @@
|
||||
-- FLOW-TWO fixture — rates for every sub-leg the segment-reuse specs book.
|
||||
--
|
||||
-- seed-import-corridor.sql only rates the four end-to-end pairs its own specs
|
||||
-- use (DJIB_PORT/NAGAD × MOJO/KALITY). Flow-two books MID-corridor legs
|
||||
-- (NAGAD→E2E_AWASH, DIRE_DAWA→MOJO, …) and pricing hard-blocks a booking on a
|
||||
-- pair with no LIVE rate — so every ordered pair of the 6-stop corridor gets a
|
||||
-- rate here. Distance-free flat values: these specs assert capacity, never money.
|
||||
--
|
||||
-- Corridor stop order (see CORRIDOR in import-utils.ts):
|
||||
-- A=DJIB_PORT B=NAGAD C=DIRE_DAWA D=E2E_AWASH E=MOJO F=KALITY
|
||||
--
|
||||
-- Idempotent: every insert is guarded by NOT EXISTS on the same key the app
|
||||
-- resolves rates by, so re-running before every spec is free.
|
||||
|
||||
-- Ordered corridor pairs (i < j), as (from, to). A plain (non-ON COMMIT DROP)
|
||||
-- temp table: db:seedFile sends the whole file as ONE implicit transaction, and
|
||||
-- it is dropped explicitly at the end so a re-run on the same pooled session
|
||||
-- doesn't hit "relation already exists".
|
||||
DROP TABLE IF EXISTS flow2_pairs;
|
||||
CREATE TEMP TABLE flow2_pairs AS
|
||||
WITH stops(code, seq) AS (
|
||||
VALUES ('DJIB_PORT', 1), ('NAGAD', 2), ('DIRE_DAWA', 3),
|
||||
('E2E_AWASH', 4), ('MOJO', 5), ('KALITY', 6)
|
||||
)
|
||||
SELECT a.code AS from_code, b.code AS to_code,
|
||||
(b.seq - a.seq) AS legs
|
||||
FROM stops a JOIN stops b ON b.seq > a.seq;
|
||||
|
||||
-- 1. Freight rates. IMPORT direction (a DJ origin) prices as CONTAINER_IMPORT /
|
||||
-- BULK_IMPORT; a wholly-Ethiopian pair is DOMESTIC and prices as INTERCITY_*.
|
||||
-- resolveTradeDirectionForBooking derives the direction from the yards'
|
||||
-- countries, so the rate_type must follow the same rule or pricing 404s.
|
||||
INSERT INTO freight.rates
|
||||
(id, rate_type, applies_to, trigger, currency, rate_value, rate_unit, status,
|
||||
origin_yard_id, destination_yard_id, proposed_by_staff_id)
|
||||
SELECT gen_random_uuid(), v.rate_type, v.applies_to, 'ALWAYS', 'USD',
|
||||
v.value, v.unit, 'LIVE', a.id, b.id, u.id
|
||||
FROM flow2_pairs p
|
||||
CROSS JOIN LATERAL (VALUES
|
||||
(CASE WHEN p.from_code = 'DJIB_PORT' THEN 'CONTAINER_IMPORT'
|
||||
ELSE 'INTERCITY_CONTAINER' END,
|
||||
CASE WHEN p.from_code = 'DJIB_PORT' THEN 'CONTAINER' ELSE 'INTERCITY' END,
|
||||
100 * p.legs, 'PER_CONTAINER'),
|
||||
(CASE WHEN p.from_code = 'DJIB_PORT' THEN 'BULK_IMPORT'
|
||||
ELSE 'INTERCITY_BULK' END,
|
||||
CASE WHEN p.from_code = 'DJIB_PORT' THEN 'BULK' ELSE 'INTERCITY' END,
|
||||
5 * p.legs, 'PER_TON')
|
||||
) AS v(rate_type, applies_to, value, unit)
|
||||
JOIN freight.yards a ON a.code = p.from_code
|
||||
JOIN freight.yards b ON b.code = p.to_code
|
||||
JOIN iam.users u ON u.email = 'operation@edr.local'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.rates r
|
||||
WHERE r.rate_type = v.rate_type
|
||||
AND r.origin_yard_id = a.id AND r.destination_yard_id = b.id
|
||||
AND r.deleted_at IS NULL
|
||||
);
|
||||
|
||||
-- 2. Customs-clearance fees on the same pairs, per container type. Only the
|
||||
-- import pairs need them (a DOMESTIC leg is never customs-cleared), but a
|
||||
-- customs booking with no fee row hard-blocks at pricing — see the note in
|
||||
-- seed-import-corridor.sql section 7b.
|
||||
INSERT INTO freight.rates
|
||||
(id, rate_type, applies_to, trigger, currency, rate_value, rate_unit, status,
|
||||
trade_direction, container_type_id, origin_yard_id, destination_yard_id,
|
||||
proposed_by_staff_id)
|
||||
SELECT gen_random_uuid(), 'CUSTOMS_CLEARANCE', 'OTHER', 'CUSTOMS_CLEARANCE',
|
||||
'USD', 50, 'PER_CONTAINER', 'LIVE', 'IMPORT', ct.id, a.id, b.id, u.id
|
||||
FROM flow2_pairs p
|
||||
JOIN freight.yards a ON a.code = p.from_code
|
||||
JOIN freight.yards b ON b.code = p.to_code
|
||||
JOIN freight.container_types ct ON ct.size_ft IN (20, 40) AND ct.is_active
|
||||
JOIN iam.users u ON u.email = 'operation@edr.local'
|
||||
WHERE p.from_code = 'DJIB_PORT'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM freight.rates r
|
||||
WHERE r.rate_type = 'CUSTOMS_CLEARANCE'
|
||||
AND r.container_type_id = ct.id
|
||||
AND r.origin_yard_id = a.id AND r.destination_yard_id = b.id
|
||||
AND r.deleted_at IS NULL
|
||||
);
|
||||
|
||||
-- 3. Yard distances on every pair. Pricing and the available-days lookup both
|
||||
-- walk yard_distances; a missing pair reads as "not on the network".
|
||||
INSERT INTO freight.yard_distances (id, from_yard_id, to_yard_id, distance_km)
|
||||
SELECT gen_random_uuid(), a.id, b.id, 100 * p.legs
|
||||
FROM flow2_pairs p
|
||||
JOIN freight.yards a ON a.code = p.from_code
|
||||
JOIN freight.yards b ON b.code = p.to_code
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.yard_distances d
|
||||
WHERE d.from_yard_id = a.id AND d.to_yard_id = b.id AND d.deleted_at IS NULL
|
||||
);
|
||||
|
||||
DROP TABLE flow2_pairs;
|
||||
108
e2e/freight/cypress/fixtures/seed-flow2-mixed-train.sql
Normal file
108
e2e/freight/cypress/fixtures/seed-flow2-mixed-train.sql
Normal file
@@ -0,0 +1,108 @@
|
||||
-- FLOW-TWO fixture — a MIXED-TYPE consist, for the wagon-pool scenario (TC-08).
|
||||
--
|
||||
-- Run AFTER seed-import-corridor.sql and seed-g1-train.sql. Idempotent.
|
||||
--
|
||||
-- WHY A SEPARATE TRAIN
|
||||
--
|
||||
-- TRN-G1-1 is 53 × NW5 — one wagon type. On that consist "container booking
|
||||
-- can't consume flatbed wagons" is untestable: there is only one pool, so the
|
||||
-- abstract slot count and the physical stock count are the same number and a
|
||||
-- broken pool separation is invisible.
|
||||
--
|
||||
-- TRN-F2-MIX splits the consist across two types the cargo allow-lists do NOT
|
||||
-- share:
|
||||
-- 30 × NW5 — the type 20FT/40FT containers ride (seed-import-corridor 2b)
|
||||
-- 20 × PW2 — the type E2E_IMP_GRAINS bulk rides (seed-import-corridor 4)
|
||||
--
|
||||
-- So 50 abstract slots, but a container booking may only ever draw on 30 of
|
||||
-- them and a grains booking on 20. A 40-wagon container booking sees "50 free"
|
||||
-- from CorridorBudget and must still be refused/split by WagonStockLedger —
|
||||
-- which is the whole assertion (wagon-stock-ledger.util.ts header: "money taken
|
||||
-- for space that never existed").
|
||||
--
|
||||
-- LENGTH MUST NOT BIND. NW5 is 13.966 m, PW2 is 17.066 m:
|
||||
-- 30 × 13.966 + 20 × 17.066 = 418.98 + 341.32 = 760.3 m
|
||||
-- which is EXACTLY over a 760 m loco. The locos below run 900 m so the length
|
||||
-- axis stays slack and TC-08 fails only on the pool separation it is testing.
|
||||
-- Pull: 9000T against 50 wagons of tare+cargo is far under. Slots and TYPE bind.
|
||||
|
||||
-- 1. Locomotive pair, 900 m so the mixed consist's length never binds.
|
||||
INSERT INTO freight.locomotives
|
||||
(id, code, max_pull_weight_tons, max_train_length_meters,
|
||||
overage_tolerance_tons, current_yard_id)
|
||||
SELECT gen_random_uuid(), v.code, 9000, 900, 0, y.id
|
||||
FROM (VALUES ('LOCO-F2-A'), ('LOCO-F2-B')) AS v(code)
|
||||
JOIN freight.yards y ON y.code = 'DJIB_PORT'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.locomotives l WHERE l.code = v.code);
|
||||
|
||||
UPDATE freight.locomotives
|
||||
SET max_pull_weight_tons = 9000, max_train_length_meters = 900,
|
||||
overage_tolerance_tons = 0,
|
||||
current_yard_id = (SELECT id FROM freight.yards WHERE code = 'DJIB_PORT')
|
||||
WHERE code IN ('LOCO-F2-A', 'LOCO-F2-B')
|
||||
AND (max_pull_weight_tons IS DISTINCT FROM 9000
|
||||
OR max_train_length_meters IS DISTINCT FROM 900
|
||||
OR overage_tolerance_tons IS DISTINCT FROM 0);
|
||||
|
||||
-- 2. The train, parked at the corridor origin (a built-train schedule requires it).
|
||||
INSERT INTO freight.trains
|
||||
(id, code, train_name, capacity_tons, current_yard_id,
|
||||
import_train_number, export_train_number)
|
||||
SELECT gen_random_uuid(), 'TRN-F2-MIX', 'E2E Flow-2 Mixed Consist', 3500, y.id,
|
||||
'9302', '9301'
|
||||
FROM freight.yards y
|
||||
WHERE y.code = 'DJIB_PORT'
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.trains t WHERE t.code = 'TRN-F2-MIX');
|
||||
|
||||
INSERT INTO freight.train_locomotives (id, train_id, locomotive_id, sequence_no)
|
||||
SELECT gen_random_uuid(), t.id, l.id, v.seq
|
||||
FROM (VALUES ('LOCO-F2-A', 0), ('LOCO-F2-B', 1)) AS v(loco_code, seq)
|
||||
JOIN freight.trains t ON t.code = 'TRN-F2-MIX'
|
||||
JOIN freight.locomotives l ON l.code = v.loco_code
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.train_locomotives tl
|
||||
WHERE tl.train_id = t.id AND tl.locomotive_id = l.id
|
||||
);
|
||||
|
||||
-- 3a. Thirty NW5 (container-capable) wagons.
|
||||
INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, current_yard_id)
|
||||
SELECT gen_random_uuid(), 'WGN-F2C-' || lpad(g::text, 2, '0'), wt.id, y.id
|
||||
FROM generate_series(1, 30) AS g
|
||||
JOIN freight.wagon_types wt ON wt.code = 'NW5'
|
||||
JOIN freight.yards y ON y.code = 'DJIB_PORT'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.wagons w
|
||||
WHERE w.wagon_number = 'WGN-F2C-' || lpad(g::text, 2, '0')
|
||||
);
|
||||
|
||||
-- 3b. Twenty PW2 (bulk-grains) wagons — NOT container-capable.
|
||||
INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, current_yard_id)
|
||||
SELECT gen_random_uuid(), 'WGN-F2B-' || lpad(g::text, 2, '0'), wt.id, y.id
|
||||
FROM generate_series(1, 20) AS g
|
||||
JOIN freight.wagon_types wt ON wt.code = 'PW2'
|
||||
JOIN freight.yards y ON y.code = 'DJIB_PORT'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM freight.wagons w
|
||||
WHERE w.wagon_number = 'WGN-F2B-' || lpad(g::text, 2, '0')
|
||||
);
|
||||
|
||||
-- 4. Couple all 50, containers first. Unconditionally re-asserted: a prior run
|
||||
-- could have left one detached, and a 49-wagon consist shifts TC-08's premise.
|
||||
UPDATE freight.wagons w
|
||||
SET train_id = t.id,
|
||||
sequence_number = g.seq,
|
||||
status = 'ASSIGNED',
|
||||
current_yard_id = t.current_yard_id
|
||||
FROM freight.trains t,
|
||||
LATERAL (
|
||||
SELECT ('WGN-F2C-' || lpad(s::text, 2, '0')) AS num, s AS seq
|
||||
FROM generate_series(1, 30) AS s
|
||||
UNION ALL
|
||||
SELECT ('WGN-F2B-' || lpad(s::text, 2, '0')), 30 + s
|
||||
FROM generate_series(1, 20) AS s
|
||||
) g
|
||||
WHERE t.code = 'TRN-F2-MIX'
|
||||
AND w.wagon_number = g.num
|
||||
AND (w.train_id IS DISTINCT FROM t.id
|
||||
OR w.sequence_number IS DISTINCT FROM g.seq
|
||||
OR w.status IS DISTINCT FROM 'ASSIGNED');
|
||||
@@ -448,3 +448,16 @@ WHERE NOT EXISTS (
|
||||
AND r.origin_yard_id = a.id AND r.destination_yard_id = b.id
|
||||
AND r.deleted_at IS NULL
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- e2e window durations: 1 minute instead of the 30/60 production defaults.
|
||||
--
|
||||
-- Most specs never wait these out — closeWindowAndRunBatch clicks "Doc review
|
||||
-- complete" and endPaymentPhase pulls the deadline into the past — so this is
|
||||
-- a safety net for the paths that DO let a phase elapse on its own, not the
|
||||
-- main speed lever. That one is the 10s @Cron tick in booking-window.service.
|
||||
-- ---------------------------------------------------------------------------
|
||||
UPDATE freight.train_scheduling_global_rules
|
||||
SET doc_review_minutes = 1,
|
||||
payment_window_minutes = 1,
|
||||
export_payment_window_minutes = 1;
|
||||
|
||||
Reference in New Issue
Block a user