Files
edr-platform/apps/edr-freight-api/src/seed/yard-facilities.seeder.ts
Hagernesh ce3ef15e7c feat(yards): record which yards can load/unload cargo
Intercity cargo is loaded at its origin yard and unloaded at its destination, but
only some yards have the equipment. EDR's facilities are Indode, Sebeta, Modjo,
Adama and Dire Dawa — and the set grows, so it has to be data.

- yards.has_facility marks a yard as a load/unload point; the new yard_facilities
  record says what it can do. Only Indode stores cargo (has_warehouse), so only it
  accrues storage/demurrage — the rest just move cargo on and off the train.
- facility_handling_events records each load/unload and carries its GRN.
  warehouse_inventory cannot: its warehouse/yard/zone are NOT NULL, so a facility
  without a warehouse could never have a row. inventory_id links to the storage
  record when there is one.
- YardFacilitiesService.facilityForYard is the single resolver the handling flows
  share, so they cannot drift on what a facility is.
- The seeder flags EXISTING yards and creates none. The codes are historical and
  do not read like the facility names — Indode is KALITY ("Gelan Multi Purpose
  Port (Indode)") and Sebeta is LEGACY_DEST — so it maps by code. Creating fresh
  INDODE/SEBETA yards would have split data that routes and bookings already
  reference.

Negad is deliberately absent: NAGAD ("DCT/SGDT") is in Djibouti while
NEGAD_FY_BCC is in Ethiopia and inactive, and which one is the intercity facility
is unsettled.

No behaviour change yet — nothing reads has_facility until the gate lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 10:40:16 +00:00

68 lines
2.7 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { DataSource } from 'typeorm';
/**
* EDR's load/unload facilities, mapped onto the yards that already represent them.
*
* The codes are historical and don't read like the facility names, so map by code
* and never by label: Indode is `KALITY` ("Gelan Multi Purpose Port (Indode)") and
* Sebeta is `LEGACY_DEST` ("Sebeta"). Creating fresh INDODE/SEBETA yards would
* split data that existing routes and bookings already point at.
*
* Only Indode stores cargo, so it is the only facility with a warehouse — the rest
* move cargo on and off the train, which is why they accrue no storage/demurrage.
*
* Negad is deliberately absent: there are two candidates (`NAGAD` "DCT/SGDT" in
* Djibouti and `NEGAD_FY_BCC` in Ethiopia, currently inactive) and it is not yet
* settled which is the intercity facility.
*/
const FACILITY_YARDS: Array<{ code: string; facility: string; hasWarehouse: boolean }> = [
{ code: 'KALITY', facility: 'Indode', hasWarehouse: true },
{ code: 'LEGACY_DEST', facility: 'Sebeta', hasWarehouse: false },
{ code: 'MOJO', facility: 'Modjo', hasWarehouse: false },
{ code: 'ADAMA', facility: 'Adama', hasWarehouse: false },
{ code: 'DIRE_DAWA', facility: 'Dire Dawa', hasWarehouse: false },
];
@Injectable()
export class YardFacilitiesSeeder {
private readonly logger = new Logger(YardFacilitiesSeeder.name);
constructor(private readonly dataSource: DataSource) {}
/**
* Idempotent: flags existing yards and upserts their facility record. Creates no
* yards — a missing code is logged and skipped rather than invented.
*/
async run(): Promise<void> {
for (const { code, facility, hasWarehouse } of FACILITY_YARDS) {
const [yard]: Array<{ id: string }> = await this.dataSource.query(
`SELECT id FROM freight.yards WHERE code = $1 AND deleted_at IS NULL`,
[code],
);
if (!yard) {
this.logger.warn(`Yard ${code} (${facility}) not found — skipping facility flag`);
continue;
}
await this.dataSource.query(
`UPDATE freight.yards
SET has_facility = true, updated_at = NOW()
WHERE id = $1 AND has_facility = false`,
[yard.id],
);
await this.dataSource.query(
`INSERT INTO freight.yard_facilities (yard_id, has_warehouse, equipment_notes)
VALUES ($1, $2, $3)
ON CONFLICT (yard_id) WHERE deleted_at IS NULL
DO UPDATE SET has_warehouse = EXCLUDED.has_warehouse, updated_at = NOW()`,
[yard.id, hasWarehouse, `${facility} load/unload facility`],
);
}
this.logger.log(
`Yard facilities seeded: ${FACILITY_YARDS.map((f) => f.facility).join(', ')}`,
);
}
}