diff --git a/apps/edr-freight-api/src/migrations/2290000000000-SeedWagonYardDoraleh.ts b/apps/edr-freight-api/src/migrations/2290000000000-SeedWagonYardDoraleh.ts new file mode 100644 index 000000000..7e12efdbb --- /dev/null +++ b/apps/edr-freight-api/src/migrations/2290000000000-SeedWagonYardDoraleh.ts @@ -0,0 +1,68 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Stand the whole wagon fleet in Doraleh. + * + * Runs AFTER SeedEdrWagonFleetErNumbering2260000000000, which recreates every + * wagon with a NULL yard — so this must stay later in timestamp order. + * + * A wagon with no yard cannot be coupled to a train (the train builder only + * offers AVAILABLE wagons standing in the train's own yard), which left the + * seeded fleet unusable. Doraleh is the Djibouti-side port yard the import runs + * originate from. + * + * The yard is created when absent: environments disagree about which yards + * exist, so this cannot assume one is there. + */ +const YARD_CODE = 'DORALEH'; + +export class SeedWagonYardDoraleh2290000000000 implements MigrationInterface { + name = 'SeedWagonYardDoraleh2290000000000'; + + public async up(queryRunner: QueryRunner): Promise { + // Ensure the yard exists and is usable. Deliberately does NOT overwrite an + // existing label/country — a deployment that already calls this yard + // something else keeps its own naming. + await queryRunner.query( + ` + INSERT INTO freight.yards (code, label, country, is_active, display_order) + VALUES ($1, 'Doraleh', 'Djibouti', true, 12) + ON CONFLICT (code) DO UPDATE SET + is_active = true, + deleted_at = NULL, + updated_at = now(); + `, + [YARD_CODE], + ); + + const [yard] = await queryRunner.query( + `SELECT id FROM freight.yards WHERE code = $1 AND deleted_at IS NULL LIMIT 1;`, + [YARD_CODE], + ); + + if (!yard?.id) { + throw new Error(`yard_missing:${YARD_CODE}`); + } + + // Whole fleet — a wagon already coupled to a built train follows the train, + // so leave those where they stand. + await queryRunner.query( + ` + UPDATE freight.wagons + SET current_yard_id = $1::uuid, + updated_at = now() + WHERE train_id IS NULL; + `, + [yard.id], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Back to the state SeedEdrWagonFleetErNumbering leaves them in. + await queryRunner.query(` + UPDATE freight.wagons + SET current_yard_id = NULL + WHERE train_id IS NULL; + `); + } +} diff --git a/apps/edr-freight-api/src/scripts/seed-edr-wagons.ts b/apps/edr-freight-api/src/scripts/seed-edr-wagons.ts index eb39bbc87..0c8374786 100644 --- a/apps/edr-freight-api/src/scripts/seed-edr-wagons.ts +++ b/apps/edr-freight-api/src/scripts/seed-edr-wagons.ts @@ -2,6 +2,7 @@ import { AppDataSource } from '../data-source'; import { SeedEdrWagonFleetErNumbering2260000000000 } from '../migrations/2260000000000-SeedEdrWagonFleetErNumbering'; import { AddWagonTrainNumbers2270000000000 } from '../migrations/2270000000000-AddWagonTrainNumbers'; import { SeedWagonRunNumbers2280000000000 } from '../migrations/2280000000000-SeedWagonRunNumbers'; +import { SeedWagonYardDoraleh2290000000000 } from '../migrations/2290000000000-SeedWagonYardDoraleh'; async function seedEdRWagons() { await AppDataSource.initialize(); @@ -12,12 +13,14 @@ async function seedEdRWagons() { await queryRunner.connect(); await queryRunner.startTransaction(); - // Fleet first (recreates every wagon with NULL runs), then the columns are - // ensured to exist, then the run roster is applied on top. Same order the - // migrations run in, so the script and a fresh migrate agree. + // Fleet first (recreates every wagon with NULL yard + NULL runs), then the + // columns are ensured to exist, then the run roster and the yard are applied + // on top. Same order the migrations run in, so the script and a fresh + // migrate agree. await new SeedEdrWagonFleetErNumbering2260000000000().up(queryRunner); await new AddWagonTrainNumbers2270000000000().up(queryRunner); await new SeedWagonRunNumbers2280000000000().up(queryRunner); + await new SeedWagonYardDoraleh2290000000000().up(queryRunner); const summary = await queryRunner.query(` SELECT @@ -27,7 +30,7 @@ async function seedEdRWagons() { MIN(w.wagon_number) AS first_wagon, MAX(w.wagon_number) AS last_wagon, COUNT(*) FILTER (WHERE w.status = 'AVAILABLE')::int AS available, - COUNT(*) FILTER (WHERE w.current_yard_id IS NULL)::int AS unassigned_yard, + COUNT(*) FILTER (WHERE w.current_yard_id IS NULL)::int AS no_yard, COUNT(*) FILTER (WHERE w.export_train_number IS NOT NULL)::int AS on_a_run FROM freight.wagons w JOIN freight.wagon_types wt ON wt.id = w.wagon_type_id @@ -43,6 +46,16 @@ async function seedEdRWagons() { FROM freight.wagons; `); + const yards = await queryRunner.query(` + SELECT + COALESCE(y.label, '(no yard)') AS yard, + COUNT(*)::int AS wagons + FROM freight.wagons w + LEFT JOIN freight.yards y ON y.id = w.current_yard_id + GROUP BY y.label + ORDER BY 2 DESC; + `); + const runs = await queryRunner.query(` SELECT export_train_number AS export_run, @@ -60,6 +73,8 @@ async function seedEdRWagons() { console.table(summary); console.log('Run roster (export/import pairs):'); console.table(runs); + console.log('Fleet by yard:'); + console.table(yards); console.log( `Seeded EDR wagon fleet — ${totals.total} wagons total (expected 1100), ` + `${totals.on_a_run} on a run (expected 533).`,