import { Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; /** A yard's load/unload capability, resolved for the handling flows. */ export interface YardFacilityInfo { yardId: string; yardCode: string | null; yardLabel: string | null; /** The yard can load/unload cargo at all. */ hasFacility: boolean; /** The facility stores cargo — enables the warehouse flow (storage, demurrage). */ hasWarehouse: boolean; /** Containers need a reach stacker/gantry — not every facility has one. */ handlesContainer: boolean; handlesBulk: boolean; } /** * Which yards can handle cargo, and what kind. * * A yard is a load/unload point when `yards.has_facility` is set; the matching * `yard_facilities` record says what it can actually do — whether it stores cargo * (storage/demurrage), and which freight types its equipment can lift. Containers * need a reach stacker or gantry, so only Indode, Modjo and Dire Dawa take them; * bulk is handled at all five. * * This is the single resolver the journey and handling flows use, so they can't * drift on what a facility is or what it can lift. */ @Injectable() export class YardFacilitiesService { constructor(private readonly dataSource: DataSource) {} private readonly SELECT = ` SELECT y.id AS "yardId", y.code AS "yardCode", y.label AS "yardLabel", y.has_facility AS "hasFacility", f.has_warehouse AS "hasWarehouse", f.handles_container AS "handlesContainer", f.handles_bulk AS "handlesBulk" FROM freight.yards y LEFT JOIN freight.yard_facilities f ON f.yard_id = y.id AND f.deleted_at IS NULL AND f.is_active = true`; private toInfo(row: { yardId: string; yardCode: string | null; yardLabel: string | null; hasFacility: boolean; hasWarehouse: boolean | null; handlesContainer: boolean | null; handlesBulk: boolean | null; }): YardFacilityInfo { // No facility record means no capability, whatever the flag says. const hasFacility = Boolean(row.hasFacility); return { yardId: row.yardId, yardCode: row.yardCode, yardLabel: row.yardLabel, hasFacility, hasWarehouse: hasFacility && Boolean(row.hasWarehouse), handlesContainer: hasFacility && Boolean(row.handlesContainer), handlesBulk: hasFacility && Boolean(row.handlesBulk), }; } /** Resolve a yard's handling capability. Null when the yard doesn't exist. */ async facilityForYard(yardId: string): Promise { const [row] = await this.dataSource.query( `${this.SELECT} WHERE y.id = $1 AND y.deleted_at IS NULL`, [yardId], ); return row ? this.toInfo(row) : null; } /** Every yard that can load/unload, for pickers and the intercity queues. */ async listFacilityYards(): Promise { const rows = await this.dataSource.query( `${this.SELECT} WHERE y.deleted_at IS NULL AND y.is_active = true AND y.has_facility = true ORDER BY y.display_order ASC, y.label ASC`, ); return rows.map((r: Parameters[0]) => this.toInfo(r)); } /** * Can this facility lift this cargo? Keeps the freight-type rule in one place * so callers can't get it subtly wrong. */ canHandleFreight( facility: YardFacilityInfo | null, freightType: string | null | undefined, ): boolean { if (!facility?.hasFacility) return false; return String(freightType).toUpperCase() === 'CONTAINER' ? facility.handlesContainer : facility.handlesBulk; } }