per-user trade-direction access scope

This commit is contained in:
Marshal
2026-08-02 22:29:58 +00:00
parent c055abe8c1
commit f4fd469643
47 changed files with 1451 additions and 107 deletions

View File

@@ -13,8 +13,20 @@ export interface YardFacilityInfo {
/** 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.
*
@@ -38,7 +50,11 @@ export class YardFacilitiesService {
y.has_facility AS "hasFacility",
f.has_warehouse AS "hasWarehouse",
f.handles_container AS "handlesContainer",
f.handles_bulk AS "handlesBulk"
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`;
@@ -51,17 +67,33 @@ export class YardFacilitiesService {
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: hasFacility && Boolean(row.handlesContainer),
handlesBulk: hasFacility && Boolean(row.handlesBulk),
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),
};
}
@@ -97,4 +129,26 @@ export class YardFacilitiesService {
? 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;
}
}