import { BaseEntity } from '@edr/api-common'; import type { ScheduleTradeDirection } from '@edr/types'; import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; import { Yard } from '../../rule-engine/entities/yard.entity'; import { RouteMilestone } from './route-milestone.entity'; export type RouteStatus = 'AVAILABLE' | 'MAINTENANCE' | 'DAMAGED' | 'STOP_WORKING'; @Entity({ schema: 'freight', name: 'routes' }) @Index(['status']) export class Route extends BaseEntity { @Column({ name: 'origin_yard_id', type: 'uuid' }) originYardId!: string; @ManyToOne(() => Yard) @JoinColumn({ name: 'origin_yard_id' }) originYard?: Yard; @Column({ name: 'destination_yard_id', type: 'uuid' }) destinationYardId!: string; @ManyToOne(() => Yard) @JoinColumn({ name: 'destination_yard_id' }) destinationYard?: Yard; @Column({ name: 'status', type: 'varchar', length: 32, default: 'AVAILABLE' }) status!: RouteStatus; /** * Trade direction frozen from the yard countries at create/update * (ET→DJ = EXPORT, DJ→ET = IMPORT, same country = DOMESTIC/"Intercity"). * Consumers (scheduling, booking windows) read this instead of re-deriving. */ @Column({ name: 'direction', type: 'varchar', length: 10 }) direction!: ScheduleTradeDirection; @OneToMany(() => RouteMilestone, (milestone) => milestone.route, { cascade: false }) milestones?: RouteMilestone[]; } /** * Human-readable route label: yard names, not yard codes — "Addis Ababa → Dire Dawa", * not "ADDIS_ABABA → DIRE_DAWA". A yard's display name is its `label`; `code` is the * machine identifier and is only a fallback for a yard missing one. * * When the route's milestones are loaded (with their yards), the label is the FULL * ordered corridor — "Addis Ababa → Adama → Dire Dawa" — since milestones already * include the origin (first) and destination (last). Without milestones it falls * back to origin → destination. */ export function formatRouteLabel(route: { originYard?: { code?: string; label?: string } | null; destinationYard?: { code?: string; label?: string } | null; milestones?: Array<{ sequenceNo: number; yard?: { code?: string; label?: string } | null; }> | null; }): string { const stops = [...(route.milestones ?? [])] .sort((a, b) => a.sequenceNo - b.sequenceNo) .map((m) => m.yard?.label ?? m.yard?.code) .filter((name): name is string => Boolean(name)); if (stops.length >= 2) return stops.join(' → '); const origin = route.originYard?.label ?? route.originYard?.code ?? 'Origin'; const dest = route.destinationYard?.label ?? route.destinationYard?.code ?? 'Destination'; return `${origin} → ${dest}`; } export function totalRouteDistanceKm( milestones: Array<{ distanceKm?: number | string | null }>, ): number { return milestones.reduce((sum, m) => sum + Number(m.distanceKm ?? 0), 0); }