mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 08:25:43 +00:00
add Group 9 delivery scenarios for self-haul and last-mile paths
- Implemented G9·S38 tests for customer self-haul truck assignments, ensuring compliance with container limits and truck assignments. - Added G9·S39 tests for last-mile delivery, self-haul, and yard pickup, verifying independent paths for multiple bookings on the same train. - Created seed data for Group 1 and Group 2 scenarios, ensuring proper setup for weight and capacity tests. - Updated booking interface to deprecate in favor of for better clarity in allocations.
This commit is contained in:
174
e2e/freight/cypress/fixtures/seed-g1-train.sql
Normal file
174
e2e/freight/cypress/fixtures/seed-g1-train.sql
Normal file
@@ -0,0 +1,174 @@
|
||||
-- Arrange-data for the GROUP 1 visual scenario specs (flows/g1_*.cy.ts).
|
||||
-- Run AFTER seed-import-corridor.sql (it provides the corridor yards, the
|
||||
-- 20FT/40FT container types + NW5 allow-list, the yard distances and the LIVE
|
||||
-- CONTAINER_IMPORT rates every booking here prices against). Idempotent.
|
||||
--
|
||||
-- WHY A BUILT TRAIN
|
||||
--
|
||||
-- The Group 1 scenarios are written against a 53-wagon train. A loco-pair
|
||||
-- schedule cannot hold that number: syncScheduleMaxWagons recomputes
|
||||
-- max_wagons as floor(locoLength / shortest active wagon length), which the
|
||||
-- corridor fixture deliberately pins at floor(760 / 13.966) = 54. A BUILT
|
||||
-- train is exempt — booking-batch.service.ts:4152 takes the physical consist
|
||||
-- count first and only falls back to the loco-derived figure:
|
||||
--
|
||||
-- const maxWagons = physicalWagons ?? capacityLimits(loco).base.wagons;
|
||||
--
|
||||
-- So 53 physically coupled wagons IS the capacity, and it survives the tick.
|
||||
-- This is also what makes the specs' visual config phase load-bearing rather
|
||||
-- than decorative: the consist staff marshal is the number the engine fills.
|
||||
--
|
||||
-- WEIGHT MUST NOT BIND IN GROUP 1
|
||||
--
|
||||
-- Group 1 tests slot arithmetic (fill, split, waitlist, priority). Weight is
|
||||
-- Group 2's axis and must stay slack here or a scenario would fail for the
|
||||
-- wrong reason. The locos below pull 9000T; a full 53-wagon board at the 10T
|
||||
-- default VGM the specs book weighs
|
||||
-- 53 × 22.4T tare + 106 × 10T cargo = 1187.2 + 1060 = 2247.2T — well under.
|
||||
-- Length: 53 × 13.966 = 740.2m under the 760m cap. Slots bind, nothing else.
|
||||
--
|
||||
-- DEDICATED CODES
|
||||
--
|
||||
-- LOCO-G1-*, TRN-G1-1, WGN-G1-* are used by NO other spec. seed-import-corridor
|
||||
-- re-parks loose NW5/CW4 stock between runs and sweeps whichever wagons sort
|
||||
-- last into other yards, so sharing the general pool would let a sibling spec
|
||||
-- steal this consist mid-run (see the header of seed-adjust-consist.sql for the
|
||||
-- same hazard). Coupled wagons (train_id set) are never swept.
|
||||
|
||||
-- 1. Locomotive pair at DJIB_PORT — the import corridor's origin yard, which a
|
||||
-- built-train schedule requires the train to be parked at. 9000T pull and 760m
|
||||
-- keep both non-slot axes slack (see header). No overage tolerance configured:
|
||||
-- Group 1 must never be rescued by tolerance, and Group 2 sets its own.
|
||||
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, 760, 0, y.id
|
||||
FROM (VALUES ('LOCO-G1-A'), ('LOCO-G1-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);
|
||||
|
||||
-- Re-assert limits every seed: a prior run's row may predate these numbers.
|
||||
UPDATE freight.locomotives
|
||||
SET max_pull_weight_tons = 9000, max_train_length_meters = 760,
|
||||
overage_tolerance_tons = 0,
|
||||
current_yard_id = (SELECT id FROM freight.yards WHERE code = 'DJIB_PORT')
|
||||
WHERE code IN ('LOCO-G1-A', 'LOCO-G1-B')
|
||||
AND (max_pull_weight_tons IS DISTINCT FROM 9000
|
||||
OR max_train_length_meters IS DISTINCT FROM 760
|
||||
OR overage_tolerance_tons IS DISTINCT FROM 0);
|
||||
|
||||
-- 2. The built train, parked at the corridor 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-G1-1', 'E2E Group-1 Container Carrier', 3500, y.id,
|
||||
'9202', '9201'
|
||||
FROM freight.yards y
|
||||
WHERE y.code = 'DJIB_PORT'
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.trains t WHERE t.code = 'TRN-G1-1');
|
||||
|
||||
-- 3. Couple the locomotive pair to the train.
|
||||
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-G1-A', 0), ('LOCO-G1-B', 1)) AS v(loco_code, seq)
|
||||
JOIN freight.trains t ON t.code = 'TRN-G1-1'
|
||||
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
|
||||
);
|
||||
|
||||
-- 4. Fifty-three dedicated NW5 flat wagons at DJIB_PORT. NW5 is the type the
|
||||
-- 20FT/40FT container types are allow-listed onto (seed-import-corridor 2b),
|
||||
-- so a container booking can actually be allocated onto them.
|
||||
INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, current_yard_id)
|
||||
SELECT gen_random_uuid(), 'WGN-G1-' || lpad(g::text, 2, '0'), wt.id, y.id
|
||||
FROM generate_series(1, 53) 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-G1-' || lpad(g::text, 2, '0')
|
||||
);
|
||||
|
||||
-- Couple all 53 onto the train, in order. Unconditionally re-asserted: a prior
|
||||
-- run's adjust-consist or an allocation left mid-flight could have detached one,
|
||||
-- and a 52-wagon consist would silently shift every scenario's arithmetic by a
|
||||
-- slot (the exact-fit cases in S1/S2 would stop being exact).
|
||||
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-G1-' || lpad(s::text, 2, '0')) AS num, s AS seq
|
||||
FROM generate_series(1, 53) AS s
|
||||
) g
|
||||
WHERE t.code = 'TRN-G1-1'
|
||||
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. A second built train for the multi-schedule scenarios (Group 4 reuses this
|
||||
-- fixture; Group 1 never schedules it). Same 53-wagon shape so "two identical
|
||||
-- trains on one day" is a true statement about capacity.
|
||||
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, 760, 0, y.id
|
||||
FROM (VALUES ('LOCO-G1-C'), ('LOCO-G1-D')) 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);
|
||||
|
||||
INSERT INTO freight.trains
|
||||
(id, code, train_name, capacity_tons, current_yard_id,
|
||||
import_train_number, export_train_number)
|
||||
SELECT gen_random_uuid(), 'TRN-G1-2', 'E2E Group-1 Container Carrier II', 3500, y.id,
|
||||
'9204', '9203'
|
||||
FROM freight.yards y
|
||||
WHERE y.code = 'DJIB_PORT'
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.trains t WHERE t.code = 'TRN-G1-2');
|
||||
|
||||
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-G1-C', 0), ('LOCO-G1-D', 1)) AS v(loco_code, seq)
|
||||
JOIN freight.trains t ON t.code = 'TRN-G1-2'
|
||||
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
|
||||
);
|
||||
|
||||
INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, current_yard_id)
|
||||
SELECT gen_random_uuid(), 'WGN-G2-' || lpad(g::text, 2, '0'), wt.id, y.id
|
||||
FROM generate_series(1, 53) 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-G2-' || lpad(g::text, 2, '0')
|
||||
);
|
||||
|
||||
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-G2-' || lpad(s::text, 2, '0')) AS num, s AS seq
|
||||
FROM generate_series(1, 53) AS s
|
||||
) g
|
||||
WHERE t.code = 'TRN-G1-2'
|
||||
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');
|
||||
|
||||
-- NOTE on S7's government queue-jump: nothing is needed here. The bonus
|
||||
-- (GOVERNMENT_PRIORITY_BONUS = 50_000, rule-engine.service.ts:258) keys off
|
||||
-- `contracts.is_government` / `government_institution`, which the booking
|
||||
-- inherits (contract-booking.service.ts:268) — so it is set per contract by
|
||||
-- seedImportContract({ government: true }), not by fleet arrange-data.
|
||||
132
e2e/freight/cypress/fixtures/seed-g2-weight.sql
Normal file
132
e2e/freight/cypress/fixtures/seed-g2-weight.sql
Normal file
@@ -0,0 +1,132 @@
|
||||
-- Arrange-data for the GROUP 2 weight/tolerance scenarios (flows/g2_weight.cy.ts).
|
||||
-- Run AFTER seed-import-corridor.sql. Idempotent.
|
||||
--
|
||||
-- Group 2 is the only group where WEIGHT binds before slots. Group 1's train
|
||||
-- (TRN-G1-1) deliberately keeps every non-slot axis slack; this one does the
|
||||
-- opposite, so the two must never share rolling stock.
|
||||
--
|
||||
-- THE ARITHMETIC (all from train-capacity.util.ts)
|
||||
--
|
||||
-- The weight axis is GROSS — a locomotive hauls the wagon as well as its
|
||||
-- cargo (`grossWagonWeightTons` = tare + cargo). NW5 tare is 22.4T, and two
|
||||
-- 20ft containers ride one NW5. So with a heavy VGM of 28T per container:
|
||||
--
|
||||
-- wagon gross = 2 × 28 + 22.4 = 78.4T ← S9's number exactly
|
||||
--
|
||||
-- S9 base pull 3500T: 35 wagons × 78.4 = 2744.0T ✔ fits
|
||||
-- 45 wagons × 78.4 = 3528.0T ✘ over base
|
||||
-- → C is blocked on WEIGHT while 18 slots still sit empty.
|
||||
--
|
||||
-- S10 tolerance 90T → cap 3590T: 3528 ≤ 3590, so C is admitted WHOLE.
|
||||
-- Tolerance is spendable ONLY to admit a booking whole.
|
||||
--
|
||||
-- S11 after 2744T used, base room = 3500 − 2744 = 756T.
|
||||
-- 756 / 78.4 = 9.64 → a split may size at most 9 wagons (705.6T).
|
||||
-- It may NEVER reach 10 by dipping into the 90T tolerance, because
|
||||
-- sizePartialOfferWagons budgets against BASE only
|
||||
-- (train-capacity.util.ts:58, booking-batch.service.ts:4109).
|
||||
--
|
||||
-- S12 light cargo (12T/container): wagon gross = 2 × 12 + 22.4 = 46.4T.
|
||||
-- 53 × 46.4 = 2459.2T — only 70% of base. SLOTS bind, not weight.
|
||||
--
|
||||
-- TWO LOCOMOTIVE PAIRS, ONE TRAIN EACH
|
||||
--
|
||||
-- Pull weight ADDS UP across a set but length takes the MINIMUM
|
||||
-- (combinedLocomotiveLimits, train-capacity.util.ts:358), so a pair of 1750T
|
||||
-- locos gives exactly the 3500T base the scenarios assume. Weight tolerance
|
||||
-- adds up too — hence 45T each for the 90T set.
|
||||
--
|
||||
-- TRN-G2-BASE LOCO-G2-A/B 1750T each, tolerance 0 → 3500T base, no slack
|
||||
-- TRN-G2-TOL LOCO-G2-C/D 1750T each, tolerance 45T → 3500T base + 90T
|
||||
--
|
||||
-- Both carry 53 NW5 wagons so the SLOT axis is identical to Group 1 and any
|
||||
-- difference in outcome is attributable to weight alone.
|
||||
|
||||
-- 1. Locomotives. Length 760m keeps the length axis slack (53 × 13.966 =
|
||||
-- 740.2m < 760m), so only weight and slots can ever bind here.
|
||||
INSERT INTO freight.locomotives
|
||||
(id, code, max_pull_weight_tons, max_train_length_meters,
|
||||
overage_tolerance_tons, overage_tolerance_meters, current_yard_id)
|
||||
SELECT gen_random_uuid(), v.code, 1750, 760, v.tol, 0, y.id
|
||||
FROM (VALUES
|
||||
('LOCO-G2-A', 0), ('LOCO-G2-B', 0),
|
||||
('LOCO-G2-C', 45), ('LOCO-G2-D', 45)
|
||||
) AS v(code, tol)
|
||||
JOIN freight.yards y ON y.code = 'DJIB_PORT'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.locomotives l WHERE l.code = v.code);
|
||||
|
||||
-- Re-assert every seed: a prior run's row may predate these numbers, and the
|
||||
-- whole group's arithmetic depends on them being exact.
|
||||
UPDATE freight.locomotives l
|
||||
SET max_pull_weight_tons = 1750,
|
||||
max_train_length_meters = 760,
|
||||
overage_tolerance_tons = v.tol,
|
||||
overage_tolerance_meters = 0,
|
||||
current_yard_id = (SELECT id FROM freight.yards WHERE code = 'DJIB_PORT')
|
||||
FROM (VALUES
|
||||
('LOCO-G2-A', 0), ('LOCO-G2-B', 0),
|
||||
('LOCO-G2-C', 45), ('LOCO-G2-D', 45)
|
||||
) AS v(code, tol)
|
||||
WHERE l.code = v.code
|
||||
AND (l.max_pull_weight_tons IS DISTINCT FROM 1750
|
||||
OR l.max_train_length_meters IS DISTINCT FROM 760
|
||||
OR l.overage_tolerance_tons IS DISTINCT FROM v.tol);
|
||||
|
||||
-- 2. The two trains at the corridor origin.
|
||||
INSERT INTO freight.trains
|
||||
(id, code, train_name, capacity_tons, current_yard_id,
|
||||
import_train_number, export_train_number)
|
||||
SELECT gen_random_uuid(), v.code, v.name, 3500, y.id, v.imp, v.exp
|
||||
FROM (VALUES
|
||||
('TRN-G2-BASE', 'E2E Group-2 Base-Weight Carrier', '9302', '9301'),
|
||||
('TRN-G2-TOL', 'E2E Group-2 Tolerance Carrier', '9304', '9303')
|
||||
) AS v(code, name, imp, exp)
|
||||
JOIN freight.yards y ON y.code = 'DJIB_PORT'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.trains t WHERE t.code = v.code);
|
||||
|
||||
-- 3. Couple each locomotive pair to its train.
|
||||
INSERT INTO freight.train_locomotives (id, train_id, locomotive_id, sequence_no)
|
||||
SELECT gen_random_uuid(), t.id, l.id, v.seq
|
||||
FROM (VALUES
|
||||
('TRN-G2-BASE', 'LOCO-G2-A', 0), ('TRN-G2-BASE', 'LOCO-G2-B', 1),
|
||||
('TRN-G2-TOL', 'LOCO-G2-C', 0), ('TRN-G2-TOL', 'LOCO-G2-D', 1)
|
||||
) AS v(train_code, loco_code, seq)
|
||||
JOIN freight.trains t ON t.code = v.train_code
|
||||
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
|
||||
);
|
||||
|
||||
-- 4. 53 dedicated NW5 wagons per train (WGN-G2B-*, WGN-G2T-*). Same slot count
|
||||
-- as Group 1 on purpose — see the header.
|
||||
INSERT INTO freight.wagons (id, wagon_number, wagon_type_id, current_yard_id)
|
||||
SELECT gen_random_uuid(), v.prefix || lpad(g::text, 2, '0'), wt.id, y.id
|
||||
FROM generate_series(1, 53) AS g
|
||||
CROSS JOIN (VALUES ('WGN-G2B-'), ('WGN-G2T-')) AS v(prefix)
|
||||
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 = v.prefix || lpad(g::text, 2, '0')
|
||||
);
|
||||
|
||||
-- Couple them, unconditionally re-asserted: a 52-wagon consist would shift the
|
||||
-- weight-vs-slot boundary the whole group is built to probe.
|
||||
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 s AS seq,
|
||||
CASE WHEN t.code = 'TRN-G2-BASE' THEN 'WGN-G2B-' ELSE 'WGN-G2T-' END
|
||||
|| lpad(s::text, 2, '0') AS num
|
||||
FROM generate_series(1, 53) AS s
|
||||
) g
|
||||
WHERE t.code IN ('TRN-G2-BASE', 'TRN-G2-TOL')
|
||||
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');
|
||||
Reference in New Issue
Block a user