Files
edr-platform/apps/edr-freight-api/src/modules/train-schedules/entities/train-schedule.entity.ts

146 lines
5.8 KiB
TypeScript

import { BaseEntity } from '@edr/api-common';
import { TrainScheduleStatus as TrainScheduleStatusEnum } from '@edr/types';
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, OneToOne } from 'typeorm';
import { Yard } from '../../rule-engine/entities/yard.entity';
import { Route } from '../../routes/entities/route.entity';
import { TrainSet } from '../../train-sets/entities/train-set.entity';
import { TrainScheduleBooking } from './train-schedule-booking.entity';
export const TRAIN_SCHEDULE_STATUSES = [
TrainScheduleStatusEnum.Draft,
TrainScheduleStatusEnum.Scheduled,
TrainScheduleStatusEnum.Dispatched,
TrainScheduleStatusEnum.Arrived,
TrainScheduleStatusEnum.Cancelled,
] as const;
export type TrainScheduleStatus = (typeof TRAIN_SCHEDULE_STATUSES)[number];
@Entity({ schema: 'freight', name: 'train_schedules' })
@Index(['scheduledDepartureDate'])
@Index(['status'])
export class TrainSchedule extends BaseEntity {
@Column({ name: 'train_set_id', type: 'uuid', unique: true })
trainSetId!: string;
@OneToOne(() => TrainSet, (trainSet) => trainSet.trainSchedule)
@JoinColumn({ name: 'train_set_id' })
trainSet?: TrainSet;
@Column({ name: 'route_id', type: 'uuid', nullable: true })
routeId?: string | null;
@ManyToOne(() => Route)
@JoinColumn({ name: 'route_id' })
route?: Route | null;
@Column({ name: 'origin_station_id', type: 'uuid' })
originStationId!: string;
@ManyToOne(() => Yard)
@JoinColumn({ name: 'origin_station_id' })
originStation?: Yard;
@Column({ name: 'destination_station_id', type: 'uuid' })
destinationStationId!: string;
@ManyToOne(() => Yard)
@JoinColumn({ name: 'destination_station_id' })
destinationStation?: Yard;
@Column({ name: 'scheduled_departure_date', type: 'timestamptz' })
scheduledDepartureDate!: Date;
@Column({ name: 'scheduled_arrival_date', type: 'timestamptz', nullable: true })
scheduledArrivalDate?: Date | null;
@Column({ name: 'status', type: 'varchar', length: 20, default: 'DRAFT' })
status!: TrainScheduleStatus;
@Column({ name: 'train_number', type: 'varchar', length: 20, nullable: true })
trainNumber?: string | null;
@Column({ name: 'direction', type: 'varchar', length: 10, nullable: true })
direction?: string | null;
@Column({ name: 'actual_departure_at', type: 'timestamptz', nullable: true })
actualDepartureAt?: Date | null;
@Column({ name: 'actual_arrival_at', type: 'timestamptz', nullable: true })
actualArrivalAt?: Date | null;
@Column({ name: 'prepared_by_user_id', type: 'uuid', nullable: true })
preparedByUserId?: string | null;
@Column({ name: 'checked_by_user_id', type: 'uuid', nullable: true })
checkedByUserId?: string | null;
@Column({ name: 'max_wagons', type: 'int', default: 53 })
maxWagons!: number;
/** OPEN = accepting/holding bookings; FULL = train filled; CLOSED = manually closed. Orthogonal to `status`. */
@Column({ name: 'booking_window_status', type: 'varchar', length: 10, default: 'OPEN' })
bookingWindowStatus!: string;
/**
* Booking-window lifecycle for the one-booking-day cycle
* (PRE_WINDOW → OPEN → DOC_REVIEW → PAYMENT → reopen | CLOSED_FOR_DAY | DONE).
* NULL on legacy and DOMESTIC schedules — the window engine ignores those.
*/
@Column({ name: 'window_phase', type: 'varchar', length: 20, nullable: true })
windowPhase?: string | null;
@Column({ name: 'window_opens_at', type: 'timestamptz', nullable: true })
windowOpensAt?: Date | null;
@Column({ name: 'window_closes_at', type: 'timestamptz', nullable: true })
windowClosesAt?: Date | null;
@Column({ name: 'doc_review_ends_at', type: 'timestamptz', nullable: true })
docReviewEndsAt?: Date | null;
/** Staff finished document review early — starts the batch/payment phase immediately. */
@Column({ name: 'doc_review_completed_at', type: 'timestamptz', nullable: true })
docReviewCompletedAt?: Date | null;
@Column({ name: 'payment_phase_ends_at', type: 'timestamptz', nullable: true })
paymentPhaseEndsAt?: Date | null;
/** 1-based count of open→settle cycles run on the booking day. */
@Column({ name: 'booking_cycle_no', type: 'int', default: 0 })
bookingCycleNo!: number;
// ── Booking-window rule snapshot ──────────────────────────────────────────
// The scheduling rule this train was created with, frozen at creation. A later
// global-rules edit applies only to FUTURE schedules — an already-open schedule
// keeps its base rule. The batch board derives its display windows (open time +
// reopen cycles) from THIS snapshot, never from the live global config. NULL on
// legacy rows created before the snapshot existed (board falls back to live cfg).
@Column({ name: 'rule_window_open_hour', type: 'int', nullable: true })
ruleWindowOpenHour?: number | null;
/** EAT hour the daily booking desk shuts (equals open hour for a 24h desk). */
@Column({ name: 'rule_window_close_hour', type: 'int', nullable: true })
ruleWindowCloseHour?: number | null;
@Column({ name: 'rule_window_duration_hours', type: 'numeric', precision: 6, scale: 4, nullable: true })
ruleWindowDurationHours?: number | null;
/**
* Frozen reopen gap = doc-review + payment minutes at creation. The board
* projects each next cycle at close + this delay, then snaps it into office hours.
*/
@Column({ name: 'rule_reopen_delay_minutes', type: 'int', nullable: true })
ruleReopenDelayMinutes?: number | null;
@Column({ name: 'rule_import_window_lead_days', type: 'int', nullable: true })
ruleImportWindowLeadDays?: number | null;
@Column({ name: 'rule_export_booking_lead_hours', type: 'int', nullable: true })
ruleExportBookingLeadHours?: number | null;
@OneToMany(() => TrainScheduleBooking, (scheduleBooking) => scheduleBooking.trainSchedule)
scheduleBookings?: TrainScheduleBooking[];
}