Files
edr-platform/apps/edr-freight-api/src/seed/yard-facilities.seeder.ts
Hagernesh a5973de6e1 feat(intercity): a facility only handles the cargo its equipment can lift
Containers need a reach stacker or gantry, so only Indode, Modjo and Dire Dawa
take them. Bulk needs far less and is handled at all five facilities. Having a
facility was previously enough to load anything, so a container booking through
Sebeta or Adama would have been accepted and then had nothing to lift it.

- yard_facilities gains handles_container / handles_bulk, both defaulting true so
  a facility handles everything unless told otherwise; the seeder states the real
  capability.
- The intercity gate now refuses cargo a facility cannot lift, saying which type,
  not just "no facility". canHandleFreight keeps that rule in the resolver so
  callers cannot get it subtly wrong.
- The intercity list resolves each end against the booking's own freight type, so
  the view flags a container booking routed through a bulk-only yard while the
  train is still coming rather than when the load is refused.

Import/export untouched — the gate is still DOMESTIC-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 07:13:51 +00:00

79 lines
3.2 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;
handlesContainer: boolean;
}> = [
// Containers need a reach stacker or gantry — only these three are equipped.
// Bulk needs far less, so every facility handles it.
{ code: 'KALITY', facility: 'Indode', hasWarehouse: true, handlesContainer: true },
{ code: 'MOJO', facility: 'Modjo', hasWarehouse: false, handlesContainer: true },
{ code: 'DIRE_DAWA', facility: 'Dire Dawa', hasWarehouse: false, handlesContainer: true },
{ code: 'LEGACY_DEST', facility: 'Sebeta', hasWarehouse: false, handlesContainer: false },
{ code: 'ADAMA', facility: 'Adama', hasWarehouse: false, handlesContainer: 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, handlesContainer } 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, handles_container, handles_bulk, equipment_notes)
VALUES ($1, $2, $3, true, $4)
ON CONFLICT (yard_id) WHERE deleted_at IS NULL
DO UPDATE SET has_warehouse = EXCLUDED.has_warehouse,
handles_container = EXCLUDED.handles_container,
handles_bulk = EXCLUDED.handles_bulk,
updated_at = NOW()`,
[yard.id, hasWarehouse, handlesContainer, `${facility} load/unload facility`],
);
}
this.logger.log(
`Yard facilities seeded: ${FACILITY_YARDS.map((f) => f.facility).join(', ')}`,
);
}
}