Files
edr-platform/apps/edr-freight-api/src/modules/routes/entities/route.entity.ts
Marshal 10111c9e01 merge
2026-07-10 11:08:30 +00:00

76 lines
2.8 KiB
TypeScript

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);
}