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; /** * The same capability split by side of the trip — loading onto a train and * receiving off one need different ground. Always false where the coarse * `handles*` flag for that type is false. */ hasContainerFacilityOrigin: boolean; hasBulkFacilityOrigin: boolean; hasContainerFacilityDestination: boolean; hasBulkFacilityDestination: boolean; } /** Which side of the trip a yard is being considered for. */ export type YardSide = 'ORIGIN' | 'DESTINATION'; /** * 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", f.has_container_facility_origin AS "hasContainerFacilityOrigin", f.has_bulk_facility_origin AS "hasBulkFacilityOrigin", f.has_container_facility_destination AS "hasContainerFacilityDestination", f.has_bulk_facility_destination AS "hasBulkFacilityDestination" 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; hasContainerFacilityOrigin: boolean | null; hasBulkFacilityOrigin: boolean | null; hasContainerFacilityDestination: boolean | null; hasBulkFacilityDestination: boolean | null; }): YardFacilityInfo { // No facility record means no capability, whatever the flag says. const hasFacility = Boolean(row.hasFacility); const handlesContainer = hasFacility && Boolean(row.handlesContainer); const handlesBulk = hasFacility && Boolean(row.handlesBulk); return { yardId: row.yardId, yardCode: row.yardCode, yardLabel: row.yardLabel, hasFacility, hasWarehouse: hasFacility && Boolean(row.hasWarehouse), handlesContainer, handlesBulk, // Gated on the coarse flag so the two can't contradict each other: a // per-side flag left set on a type the facility no longer handles at all // never resurrects that type. hasContainerFacilityOrigin: handlesContainer && Boolean(row.hasContainerFacilityOrigin), hasBulkFacilityOrigin: handlesBulk && Boolean(row.hasBulkFacilityOrigin), hasContainerFacilityDestination: handlesContainer && Boolean(row.hasContainerFacilityDestination), hasBulkFacilityDestination: handlesBulk && Boolean(row.hasBulkFacilityDestination), }; } /** 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; } /** * Can this facility take this cargo on this side of the trip? The rule behind * the contract's origin/destination yard pickers — keep it here so the API * and the forms can't drift apart on what is offerable. */ canHandleFreightOnSide( facility: YardFacilityInfo | null, freightType: string | null | undefined, side: YardSide, ): boolean { if (!facility?.hasFacility) return false; const isContainer = String(freightType).toUpperCase() === 'CONTAINER'; if (side === 'ORIGIN') { return isContainer ? facility.hasContainerFacilityOrigin : facility.hasBulkFacilityOrigin; } return isContainer ? facility.hasContainerFacilityDestination : facility.hasBulkFacilityDestination; } }