import { Injectable } from '@nestjs/common'; import { DataSource } from 'typeorm'; /** * READ-ONLY view into the train-scheduling / wagons domain for the warehouse module. * * IMPORTANT: this facade only ever runs SELECTs. The warehouse must never modify * wagon assignment, rescheduling, import_ready/export_ready, or locomotive flow. * It is intentionally decoupled (raw SQL) so it does not import the scheduling * services/entities and cannot accidentally write to them. */ export interface WagonView { id: string; wagonNumber: string; status: string; trainId: string | null; } export interface BookingScheduleView { schedule: { id: string; status: string; scheduledDepartureDate: string | null; scheduledArrivalDate: string | null; originStationId: string | null; destinationStationId: string | null; } | null; wagon: { wagonId: string | null; wagonNumber: string | null; sequenceNo: number | null; allocatedWeightTons: number | null; } | null; /** Mirror of schedule.status — the headline "where is the train" indicator. */ departureStatus: string | null; } @Injectable() export class SchedulingReadFacade { constructor(private readonly dataSource: DataSource) {} /** Look up a single physical wagon. Returns null if it does not exist. */ async findWagon(wagonId: string): Promise { const rows = await this.dataSource.query( `SELECT id, wagon_number AS "wagonNumber", status, train_id AS "trainId" FROM freight.wagons WHERE id = $1 AND deleted_at IS NULL LIMIT 1`, [wagonId], ); return rows?.[0] ?? null; } /** True when the wagon is already part of a train set (selected by an existing schedule). */ async isWagonScheduled(wagonId: string): Promise { const rows = await this.dataSource.query( `SELECT 1 FROM freight.train_set_wagons WHERE physical_wagon_id = $1 AND deleted_at IS NULL LIMIT 1`, [wagonId], ); return (rows?.length ?? 0) > 0; } /** List wagons usable for loading (available, or already assigned to a schedule). */ listLoadableWagons(): Promise { return this.dataSource.query( `SELECT id, wagon_number AS "wagonNumber", status, train_id AS "trainId" FROM freight.wagons WHERE deleted_at IS NULL AND status NOT IN ('RETIRED', 'MAINTENANCE') ORDER BY wagon_number ASC`, ); } /** * Given a booking, return its related schedule, wagon assignment and departure status. * All fields are read straight from the scheduling tables — nothing is written. */ async getBookingSchedule(bookingId: string): Promise { const scheduleRows = await this.dataSource.query( `SELECT ts.id, ts.status, ts.scheduled_departure_date AS "scheduledDepartureDate", ts.scheduled_arrival_date AS "scheduledArrivalDate", ts.origin_station_id AS "originStationId", ts.destination_station_id AS "destinationStationId" FROM freight.train_schedule_bookings tsb INNER JOIN freight.train_schedules ts ON ts.id = tsb.train_schedule_id WHERE tsb.booking_id = $1 AND ts.deleted_at IS NULL ORDER BY ts.scheduled_departure_date DESC NULLS LAST LIMIT 1`, [bookingId], ); const schedule = scheduleRows?.[0] ?? null; const wagonRows = await this.dataSource.query( `SELECT w.id AS "wagonId", w.wagon_number AS "wagonNumber", tsw.sequence_no AS "sequenceNo", wba.allocated_weight_tons AS "allocatedWeightTons" FROM freight.wagon_booking_allocations wba INNER JOIN freight.train_set_wagons tsw ON tsw.id = wba.train_set_wagon_id LEFT JOIN freight.wagons w ON w.id = tsw.physical_wagon_id WHERE wba.booking_id = $1 ORDER BY tsw.sequence_no ASC NULLS LAST LIMIT 1`, [bookingId], ); const wagon = wagonRows?.[0] ?? null; return { schedule, wagon, departureStatus: schedule?.status ?? null, }; } }