import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query, Res, } from "@nestjs/common"; import { CurrentUser } from "@edr/api-common"; import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; import type { Response } from "express"; import type { AuthUserPayload } from "../../common/resolve-auth-user-id"; import { resolveAuthUserId } from "../../common/resolve-auth-user-id"; import { TrainSchedulingManage, TrainSchedulingView, } from "../../common/booking-guards"; import { AcceptIntercityBookingsDto } from "./dto/accept-intercity-bookings.dto"; import { AssignBookingsDto } from "./dto/assign-bookings.dto"; import { AssignUnassignedBookingDto } from "./dto/assign-unassigned-booking.dto"; import { CreateContainerTrainScheduleDto } from "./dto/create-container-train-schedule.dto"; import { GetEligibleBookingsDto } from "./dto/get-eligible-bookings.dto"; import { GetEligibleBulkBookingsDto } from "./dto/get-eligible-bulk-bookings.dto"; import { GetEligibleContainerBookingsDto } from "./dto/get-eligible-container-bookings.dto"; import { PinWagonsDto } from "./dto/pin-wagons.dto"; import { MoveWagonLoadDto } from "./dto/move-wagon-load.dto"; import { UpdateContainerItemDto } from "./dto/update-container-item.dto"; import { UpdateImportLoadingStatusDto } from "./dto/update-import-loading-status.dto"; import { PreviewBulkTrainScheduleDto } from "./dto/preview-bulk-train-schedule.dto"; import { PreviewContainerTrainScheduleDto } from "./dto/preview-container-train-schedule.dto"; import { PreviewTrainScheduleDto } from "./dto/preview-train-schedule.dto"; import { RecordCheckpointDto } from "./dto/record-checkpoint.dto"; import { ImportDjiboutiActionDto, UploadImportDjiboutiDocumentDto, } from "./dto/import-djibouti-operation.dto"; import { AvailableLocomotivesQueryDto } from "./dto/available-locomotives-query.dto"; import { AdjustScheduleConsistDto } from "./dto/adjust-schedule-consist.dto"; import { AvailableTrainsQueryDto } from "./dto/available-trains-query.dto"; import { BatchBoardQueryDto } from "./dto/batch-board-query.dto"; import { BookableSchedulesQueryDto } from "./dto/bookable-schedules-query.dto"; import { ListTrainSchedulesQueryDto } from "./dto/list-train-schedules-query.dto"; import { AvailableDaysQueryDto } from "./dto/available-days-query.dto"; import { AvailableDaysForCargoQueryDto } from "./dto/available-days-for-cargo-query.dto"; import { UpdateTrainSchedulingGlobalRulesDto } from "./dto/update-train-scheduling-global-rules.dto"; import { UpdateScheduleWindowRuleDto } from "./dto/update-schedule-window-rule.dto"; import { UpdateScheduleDateDto } from "./dto/update-schedule-date.dto"; import { MaintenanceRescheduleDto } from "./dto/maintenance-reschedule.dto"; import { TrainSchedulingService } from "./train-scheduling.service"; import { BookingBatchService } from "./booking-batch.service"; import { BookingJourneyService } from "./booking-journey.service"; import { BookingWindowService } from "./booking-window.service"; import { IntercityService } from "./intercity.service"; import { BillingService } from "../billing/billing.service"; @ApiTags("train-scheduling") @ApiBearerAuth() @Controller("train-scheduling") export class TrainSchedulingController { constructor( private readonly trainSchedulingService: TrainSchedulingService, private readonly bookingBatchService: BookingBatchService, private readonly bookingWindowService: BookingWindowService, private readonly intercityService: IntercityService, private readonly bookingJourneyService: BookingJourneyService, private readonly billingService: BillingService, ) { } @Get("my-booking-windows") @ApiOperation({ summary: "Upcoming/open booking windows announced to the signed-in customer (all window-engine schedules; their own contract lanes carry a Book-now target)", }) async getMyBookingWindows(@CurrentUser() user: AuthUserPayload) { // Every customer sees announced windows; companyId (when resolvable) just // enriches lanes they hold a contract on so "Book now" can target it. const companyId = await this.billingService.resolveCompanyId( resolveAuthUserId(user), ); return this.trainSchedulingService.getBookingWindowsForCompany(companyId); } @Get("contracts/:contractId/booking-windows") @ApiOperation({ summary: "Upcoming/open booking windows on a contract's routes — gates the booking form for customer + Ethiopian GL", }) getContractBookingWindows( @Param("contractId", ParseUUIDPipe) contractId: string, ) { return this.trainSchedulingService.getBookingWindowsForContract(contractId); } @Get("booking-windows") @TrainSchedulingView() @ApiOperation({ summary: "All announced booking windows across lanes (import cycle + export FCFS), for staff dashboards", }) listBookingWindows() { return this.trainSchedulingService.listAllBookingWindows(); } @Get("global-rules") @TrainSchedulingView() @ApiOperation({ summary: "Get global train scheduling rules (singleton)" }) getGlobalRules() { return this.trainSchedulingService.getTrainSchedulingGlobalRules(); } @Patch("global-rules") @TrainSchedulingManage() @ApiOperation({ summary: "Update global train scheduling rules (singleton)" }) updateGlobalRules(@Body() dto: UpdateTrainSchedulingGlobalRulesDto) { return this.trainSchedulingService.updateTrainSchedulingGlobalRules(dto); } @Get("eligible-bookings") @TrainSchedulingView() @ApiOperation({ summary: "List eligible bookings (container and/or bulk)" }) getEligibleBookings(@Query() query: GetEligibleBookingsDto) { return this.trainSchedulingService.getEligibleBookings(query); } @Get("batch-board") @TrainSchedulingView() @ApiOperation({ summary: "Batch monitoring board: paginated import schedules (all statuses) with bookings grouped by state", }) getBatchBoard(@Query() query: BatchBoardQueryDto) { return this.bookingBatchService.getBatchBoard(query); } @Get("batch-board/:scheduleId") @TrainSchedulingView() @ApiOperation({ summary: "Batch board detail for one schedule: its booking window, in-window bookings and pending-contract bucket", }) getBatchBoardDetail(@Param("scheduleId", ParseUUIDPipe) scheduleId: string) { return this.bookingBatchService.getBatchBoardDetail(scheduleId); } @Get("available-locomotives") @TrainSchedulingView() @ApiOperation({ summary: "List AVAILABLE locomotives at the route origin yard", }) getAvailableLocomotives(@Query() query: AvailableLocomotivesQueryDto) { return this.trainSchedulingService.getAvailableLocomotivesForRoute( query.routeId, ); } @Get("available-trains") @TrainSchedulingView() @ApiOperation({ summary: "List built trains (Train Builder) schedulable on a route, annotated with yard position and future runs", }) getAvailableTrains(@Query() query: AvailableTrainsQueryDto) { return this.trainSchedulingService.getAvailableTrainsForRoute( query.routeId, ); } @Get("schedules/:id/consist") @TrainSchedulingView() @ApiOperation({ summary: "Built-train consist snapshot for a schedule: gross weight/length vs locomotive limits (incl. tolerance), trimmable + addable wagons, adjustment history", }) getScheduleConsist(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getScheduleConsist(id); } @Post("schedules/:id/adjust-consist") @TrainSchedulingManage() @ApiOperation({ summary: "Permanently trim free wagons off / couple yard wagons onto the schedule's built train (weight & length limits incl. tolerance enforced, every change logged)", }) adjustScheduleConsist( @Param("id", ParseUUIDPipe) id: string, @Body() dto: AdjustScheduleConsistDto, @CurrentUser() user: AuthUserPayload, ) { return this.trainSchedulingService.adjustScheduleConsist( id, dto, resolveAuthUserId(user), ); } @Get("bookable-schedules") // No staff guard: customers hit this while creating a booking to find OPEN // same-route schedules. Do not attach train_scheduling permissions here. @ApiOperation({ summary: "OPEN same-route schedules a new booking can target", }) getBookableSchedules(@Query() query: BookableSchedulesQueryDto) { return this.trainSchedulingService.getBookableSchedules( query.originYardId, query.destinationYardId, ); } @Get("available-days") // No staff guard: customers hit this while creating a booking to find which // DAYS have a departure on their route. Day-level pooling — no capacity is // returned, only the list of bookable days. @ApiOperation({ summary: "Distinct days with an OPEN same-route departure (day-level pool)", }) getAvailableDays(@Query() query: AvailableDaysQueryDto) { return this.trainSchedulingService.getAvailableDays( query.originYardId, query.destinationYardId, ); } @Get("available-days-for-cargo") // No staff guard: customers (portal) and GL (backoffice) both hit this while // creating a booking to find which DAYS are feasible for THIS cargo — i.e. the // route has a train with remaining capacity AND enough matching-type wagons. @ApiOperation({ summary: "Days bookable for a specific cargo (wagon + train capacity aware)", }) getAvailableDaysForCargo(@Query() query: AvailableDaysForCargoQueryDto) { return this.trainSchedulingService.getAvailableDaysForCargo({ originYardId: query.originYardId, destinationYardId: query.destinationYardId, freightType: query.freightType, cargoTypeId: query.cargoTypeId, cargoTypeCode: query.cargoTypeCode, totalWeightTons: query.totalWeightTons, containers: query.containers, containerTypeIds: query.containerTypeIds, }); } @Get("container/eligible-bookings") @TrainSchedulingView() @ApiOperation({ summary: "List eligible container bookings" }) getEligibleContainerBookings( @Query() query: GetEligibleContainerBookingsDto, ) { return this.trainSchedulingService.getEligibleContainerBookings(query); } @Get("bulk/eligible-bookings") @TrainSchedulingView() @ApiOperation({ summary: "List eligible bulk bookings" }) getEligibleBulkBookings(@Query() query: GetEligibleBulkBookingsDto) { return this.trainSchedulingService.getEligibleBulkBookings(query); } @Post("preview") @TrainSchedulingView() @ApiOperation({ summary: "Preview a mixed-capable train schedule" }) previewTrainSchedule(@Body() dto: PreviewTrainScheduleDto) { return this.trainSchedulingService.previewTrainSchedule(dto); } @Post("container/preview") @TrainSchedulingView() @ApiOperation({ summary: "Preview a container train schedule" }) previewContainerTrainSchedule(@Body() dto: PreviewContainerTrainScheduleDto) { return this.trainSchedulingService.previewContainerTrainSchedule(dto); } @Post("bulk/preview") @TrainSchedulingView() @ApiOperation({ summary: "Preview a bulk train schedule" }) previewBulkTrainSchedule(@Body() dto: PreviewBulkTrainScheduleDto) { return this.trainSchedulingService.previewBulkTrainSchedule(dto); } @Post("container/schedules") @TrainSchedulingManage() @ApiOperation({ summary: "Create a container train schedule" }) createContainerTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) { return this.trainSchedulingService.createContainerTrainSchedule(dto); } @Post("bulk/schedules") @TrainSchedulingManage() @ApiOperation({ summary: "Create a bulk train schedule" }) createBulkTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) { return this.trainSchedulingService.createContainerTrainSchedule(dto); } @Post("schedules/:id/assign-bookings") @TrainSchedulingManage() @ApiOperation({ summary: "Assign bookings to a train schedule (mixed-capable)", }) assignBookings( @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { return this.trainSchedulingService.assignBookingsToSchedule(id, dto); } @Post("container/schedules/:id/assign-bookings") @TrainSchedulingManage() @ApiOperation({ summary: "Assign container bookings to a train schedule" }) assignContainerBookings( @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { return this.trainSchedulingService.assignBookingsToSchedule( id, dto, "CONTAINER", ); } @Post("bulk/schedules/:id/assign-bookings") @TrainSchedulingManage() @ApiOperation({ summary: "Assign bulk bookings to a train schedule" }) assignBulkBookings( @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { return this.trainSchedulingService.assignBookingsToSchedule( id, dto, "BULK", ); } @Delete("schedules/:id/bookings/:bookingId") @TrainSchedulingManage() @ApiOperation({ summary: "Unassign a booking from a train schedule" }) unassignBooking( @Param("id", ParseUUIDPipe) id: string, @Param("bookingId", ParseUUIDPipe) bookingId: string, @CurrentUser() user: AuthUserPayload, ) { return this.trainSchedulingService.unassignBooking( id, bookingId, resolveAuthUserId(user), ); } @Delete("schedules/:id/wagons/:trainSetWagonId") @TrainSchedulingManage() @ApiOperation({ summary: "Remove an empty wagon slot from a train" }) removeWagonSlot( @Param("id", ParseUUIDPipe) id: string, @Param("trainSetWagonId", ParseUUIDPipe) trainSetWagonId: string, ) { return this.trainSchedulingService.removeTrainSetWagonSlot( id, trainSetWagonId, ); } @Patch("schedules/:id/container-items/:itemId") @TrainSchedulingManage() @ApiOperation({ summary: "Update a container number on a wagon slot" }) updateContainerItem( @Param("id", ParseUUIDPipe) id: string, @Param("itemId", ParseUUIDPipe) itemId: string, @Body() dto: UpdateContainerItemDto, ) { return this.trainSchedulingService.updateContainerItem(id, itemId, dto); } @Post("schedules/:id/wagons/:wagonId/move-load") @TrainSchedulingManage() @ApiOperation({ summary: "Move a wagon's whole load to another wagon (empty → move/repin, loaded → swap loads)", }) moveWagonLoad( @Param("id", ParseUUIDPipe) id: string, @Param("wagonId", ParseUUIDPipe) wagonId: string, @Body() dto: MoveWagonLoadDto, ) { return this.trainSchedulingService.moveWagonLoad(id, wagonId, dto); } @Get("schedules/:id/unassigned-bookings") @TrainSchedulingView() @ApiOperation({ summary: "Get unassigned bookings for a schedule" }) getUnassignedBookings(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getUnassignedBookings(id); } @Post("schedules/:id/assign-unassigned-booking") @TrainSchedulingManage() @ApiOperation({ summary: "Assign one linked unallocated booking to wagons (preserves existing assignments)", }) assignUnassignedBooking( @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignUnassignedBookingDto, ) { return this.trainSchedulingService.assignUnassignedBookingToWagons( id, dto.bookingId, ); } @Get("schedules/:id/composition-removals") @TrainSchedulingView() @ApiOperation({ summary: "Get removal log for a schedule" }) getCompositionRemovals(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getCompositionRemovals(id); } @Get("schedules/:id/import-loading-bookings") @TrainSchedulingView() @ApiOperation({ summary: "List import bookings eligible for loading confirmation on this schedule", }) getImportLoadingBookings(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getImportLoadingBookings(id); } @Patch("schedules/:id/import-loading-status") @TrainSchedulingManage() @ApiOperation({ summary: "Mark import bookings loaded/unloaded on this schedule (tracking only, does not affect dispatch)", }) updateImportLoadingStatus( @Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateImportLoadingStatusDto, ) { return this.trainSchedulingService.updateImportLoadingStatus(id, dto); } @Patch("schedules/:id/loading-status") @TrainSchedulingManage() @ApiOperation({ summary: "Mark bookings loaded/unloaded on this schedule (any direction, pre-dispatch only)", }) setBookingLoadingStatus( @Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateImportLoadingStatusDto, ) { return this.trainSchedulingService.setBookingLoadingStatus(id, dto); } @Post("schedules/:id/pin-wagons") @TrainSchedulingManage() @ApiOperation({ summary: "Pin physical wagons to train set slots" }) pinWagons(@Param("id", ParseUUIDPipe) id: string, @Body() dto: PinWagonsDto) { return this.trainSchedulingService.pinWagons(id, dto); } @Post("schedules/:id/finalize") @TrainSchedulingManage() @ApiOperation({ summary: "Finalize a draft train schedule" }) finalizeSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.finalizeSchedule(id); } @Post("schedules/:id/dispatch") @TrainSchedulingManage() @ApiOperation({ summary: "Dispatch a scheduled train" }) dispatchSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.dispatchSchedule(id); } @Get("intercity/bookings") @TrainSchedulingView() @ApiOperation({ summary: "Every intercity booking with its ride-along state, both yards' facility status, and where its train is", }) listIntercityBookings() { return this.intercityService.listBookings(); } @Get("schedules/:id/intercity-candidates") @TrainSchedulingView() @ApiOperation({ summary: "Waiting intercity bookings this train could carry (corridor on route) + remaining wagon/weight/length capacity", }) getIntercityCandidates(@Param("id", ParseUUIDPipe) id: string) { return this.intercityService.listCandidates(id); } @Post("schedules/:id/intercity/accept") @TrainSchedulingManage() @ApiOperation({ summary: "Accept intercity bookings onto this train (opens their pay window; capacity re-checked per booking)", }) acceptIntercityBookings( @Param("id", ParseUUIDPipe) id: string, @Body() dto: AcceptIntercityBookingsDto, ) { return this.intercityService.acceptBookings(id, dto.bookingIds); } @Get("schedules/:id/yard-work") @TrainSchedulingView() @ApiOperation({ summary: "Per-yard operator worklist: which bookings board/alight at each stop, with journey state", }) getYardWork(@Param("id", ParseUUIDPipe) id: string) { return this.bookingJourneyService.listYardWork(id); } @Post("schedules/:id/bookings/:bookingId/load") @TrainSchedulingManage() @ApiOperation({ summary: "Confirm a booking's cargo loaded at its origin yard (any direction; train must be at that yard)", }) loadScheduleBooking( @Param("id", ParseUUIDPipe) id: string, @Param("bookingId", ParseUUIDPipe) bookingId: string, ) { return this.bookingJourneyService.loadBooking(id, bookingId); } @Post("schedules/:id/bookings/:bookingId/unload") @TrainSchedulingManage() @ApiOperation({ summary: "Confirm a booking's cargo unloaded at its destination yard — per-booking arrival, may precede the train's final arrival", }) unloadScheduleBooking( @Param("id", ParseUUIDPipe) id: string, @Param("bookingId", ParseUUIDPipe) bookingId: string, ) { return this.bookingJourneyService.unloadBooking(id, bookingId); } @Post("schedules/:id/intercity/:bookingId/load") @TrainSchedulingManage() @ApiOperation({ summary: "Confirm intercity cargo loaded (train must be at the booking's origin yard)", }) loadIntercityBooking( @Param("id", ParseUUIDPipe) id: string, @Param("bookingId", ParseUUIDPipe) bookingId: string, ) { return this.intercityService.loadBooking(id, bookingId); } @Post("schedules/:id/intercity/:bookingId/unload") @TrainSchedulingManage() @ApiOperation({ summary: "Confirm intercity cargo unloaded at the booking's destination yard (completes the booking)", }) unloadIntercityBooking( @Param("id", ParseUUIDPipe) id: string, @Param("bookingId", ParseUUIDPipe) bookingId: string, ) { return this.intercityService.unloadBooking(id, bookingId); } @Get("schedules/:id/import-djibouti") @TrainSchedulingView() @ApiOperation({ summary: "Batch 7 import Djibouti gatepass/loading status" }) getImportDjiboutiOperation(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getImportDjiboutiOperation(id); } @Post("schedules/:id/import-djibouti/documents") @TrainSchedulingManage() @ApiOperation({ summary: "Upload/check an import Djibouti-side document" }) uploadImportDjiboutiDocument( @Param("id", ParseUUIDPipe) id: string, @Body() dto: UploadImportDjiboutiDocumentDto, ) { return this.trainSchedulingService.uploadImportDjiboutiDocument(id, dto); } @Post("schedules/:id/import-djibouti/gatepass-granted") @TrainSchedulingManage() @ApiOperation({ summary: "Mark import Djibouti gatepass permission granted" }) grantImportDjiboutiGatepass( @Param("id", ParseUUIDPipe) id: string, @Body() dto: ImportDjiboutiActionDto, ) { return this.trainSchedulingService.grantImportDjiboutiGatepass(id, dto); } @Post("schedules/:id/import-djibouti/ready-for-loading") @TrainSchedulingManage() @ApiOperation({ summary: "Mark import train ready for loading at Djibouti" }) markImportReadyForLoading( @Param("id", ParseUUIDPipe) id: string, @Body() dto: ImportDjiboutiActionDto, ) { return this.trainSchedulingService.markImportReadyForLoading(id, dto); } @Post("schedules/:id/import-djibouti/loaded-on-train") @TrainSchedulingManage() @ApiOperation({ summary: "Confirm import cargo loaded on train at Djibouti" }) confirmImportLoadedOnTrain( @Param("id", ParseUUIDPipe) id: string, @Body() dto: ImportDjiboutiActionDto, ) { return this.trainSchedulingService.confirmImportLoadedOnTrain(id, dto); } @Post("schedules/:id/confirm-loading") @TrainSchedulingManage() @ApiOperation({ summary: "Confirm cargo loaded on the train (any direction; unblocks import-Djibouti dispatch)", }) confirmScheduleLoading( @Param("id", ParseUUIDPipe) id: string, @Body() dto: ImportDjiboutiActionDto, ) { return this.trainSchedulingService.confirmScheduleLoading(id, dto); } @Post("schedules/:id/import-djibouti/depart") @TrainSchedulingManage() @ApiOperation({ summary: "Depart loaded import train from Djibouti" }) departImportFromDjibouti( @Param("id", ParseUUIDPipe) id: string, @Body() dto: ImportDjiboutiActionDto, ) { return this.trainSchedulingService.departImportFromDjibouti(id, dto); } @Post("schedules/:id/import-djibouti/load-list") @TrainSchedulingManage() @ApiOperation({ summary: "Generate import load list / marshalling document summary" }) generateImportLoadList( @Param("id", ParseUUIDPipe) id: string, @Body() dto: ImportDjiboutiActionDto, ) { return this.trainSchedulingService.generateImportLoadList(id, dto); } @Get("schedules/:id/import-djibouti/load-list/document") @TrainSchedulingView() @ApiOperation({ summary: "Download printable import load list / marshalling PDF" }) async importLoadListDocument( @Param("id", ParseUUIDPipe) id: string, @Res() res: Response, ) { const { filename, buffer } = await this.trainSchedulingService.importLoadListDocument(id); res.setHeader("Content-Type", "application/pdf"); res.setHeader("Content-Disposition", `inline; filename="${filename}"`); res.setHeader("Content-Length", buffer.length); return res.send(buffer); } @Get("schedules/:id/export/load-list/document") @TrainSchedulingView() @ApiOperation({ summary: "Download printable export marshalling / load list PDF" }) async exportLoadListDocument( @Param("id", ParseUUIDPipe) id: string, @Res() res: Response, ) { const { filename, buffer } = await this.trainSchedulingService.exportLoadListDocument(id); res.setHeader("Content-Type", "application/pdf"); res.setHeader("Content-Disposition", `inline; filename="${filename}"`); res.setHeader("Content-Length", buffer.length); return res.send(buffer); } // ---- batch / booking-window staff actions ---- @Post("schedules/:id/run-batch") @TrainSchedulingManage() @ApiOperation({ summary: "Manually run the batch fill for a schedule" }) async runBatch(@Param("id", ParseUUIDPipe) id: string) { await this.bookingBatchService.fillSchedule(id); return this.bookingBatchService.getBatchBoardDetail(id); } @Post("schedules/:id/run-allocation") @TrainSchedulingManage() @ApiOperation({ summary: "Run wagon-level allocation for all eligible linked bookings", }) async runAllocation(@Param("id", ParseUUIDPipe) id: string) { return this.bookingBatchService.runWagonAllocation(id); } @Patch("schedules/:id/booking-window") @TrainSchedulingManage() @ApiOperation({ summary: "Open or close a schedule booking window" }) async setBookingWindow( @Param("id", ParseUUIDPipe) id: string, @Body("status") status: "OPEN" | "CLOSED", ) { await this.trainSchedulingService.setBookingWindow( id, status === "CLOSED" ? "CLOSED" : "OPEN", ); return this.trainSchedulingService.getContainerTrainScheduleById(id); } @Patch("schedules/:id/window-rule") @TrainSchedulingManage() @ApiOperation({ summary: "Override the booking-window rule for one schedule (open/close hour, duration, doc-review, payment, lead days) — only before the window opens", }) async updateScheduleWindowRule( @Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateScheduleWindowRuleDto, ) { await this.trainSchedulingService.updateScheduleWindowRule(id, dto); return this.trainSchedulingService.getContainerTrainScheduleById(id); } @Patch("schedules/:id/schedule-date") @TrainSchedulingManage() @ApiOperation({ summary: "Reschedule a train's departure date — only before the booking window opens, and only if the new date still leaves room for the booking lead window", }) async updateScheduleDate( @Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateScheduleDateDto, ) { await this.trainSchedulingService.updateScheduleDate(id, dto); return this.trainSchedulingService.getContainerTrainScheduleById(id); } @Post("schedules/:id/maintenance") @TrainSchedulingManage() @ApiOperation({ summary: "Maintenance reschedule: move the train to a new departure with every allocated booking aboard — links, wagons and window settings unchanged", }) async maintenanceReschedule( @Param("id", ParseUUIDPipe) id: string, @Body() dto: MaintenanceRescheduleDto, ) { await this.trainSchedulingService.maintenanceReschedule(id, dto); return this.trainSchedulingService.getContainerTrainScheduleById(id); } @Post("schedules/:id/doc-review-complete") @TrainSchedulingManage() @ApiOperation({ summary: "Staff finished document review early — run the batch/payment phase now (applies to the whole route-day group)", }) async completeDocReview(@Param("id", ParseUUIDPipe) id: string) { await this.bookingWindowService.completeDocReview(id); return this.bookingBatchService.getBatchBoardDetail(id); } @Post("bookings/:bookingId/mark-paid") @TrainSchedulingManage() @ApiOperation({ summary: "Staff: mark a reserved booking paid and allocate it now", }) async markBookingPaid(@Param("bookingId", ParseUUIDPipe) bookingId: string) { await this.bookingBatchService.markPaid(bookingId); return { ok: true }; } @Post("bookings/:bookingId/expire") @TrainSchedulingManage() @ApiOperation({ summary: "Staff: expire a reservation and free its capacity", }) async expireBooking(@Param("bookingId", ParseUUIDPipe) bookingId: string) { await this.bookingBatchService.expireReservation(bookingId); return { ok: true }; } @Post("bookings/:bookingId/move-schedule") @TrainSchedulingManage() @ApiOperation({ summary: "Re-point a booking to another OPEN same-route schedule", }) async moveBookingSchedule( @Param("bookingId", ParseUUIDPipe) bookingId: string, @Body("trainScheduleId", ParseUUIDPipe) trainScheduleId: string, ) { await this.bookingBatchService.moveToSchedule(bookingId, trainScheduleId); return { ok: true }; } @Get("schedules/:id/checkpoints") @TrainSchedulingView() @ApiOperation({ summary: "Get the tracking corridor + logged checkpoints for a train", }) getScheduleCheckpoints(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getScheduleCheckpoints(id); } @Post("schedules/:id/checkpoints") @TrainSchedulingManage() @ApiOperation({ summary: "Log the train passing a station (final station triggers arrival)", }) recordCheckpoint( @Param("id", ParseUUIDPipe) id: string, @Body() dto: RecordCheckpointDto, ) { return this.trainSchedulingService.recordCheckpoint(id, dto); } @Post("schedules/:id/arrive") @TrainSchedulingManage() @ApiOperation({ summary: "Mark a dispatched train arrived (move assets to destination yard, free assets)", }) arriveSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.arriveSchedule(id); } @Get("container/schedules") @TrainSchedulingView() @ApiOperation({ summary: "List container train schedules (paginated)" }) getContainerTrainSchedules(@Query() query: ListTrainSchedulesQueryDto) { return this.trainSchedulingService.getContainerTrainSchedules(query); } @Get("bulk/schedules") @TrainSchedulingView() @ApiOperation({ summary: "List bulk train schedules (paginated)" }) getBulkTrainSchedules(@Query() query: ListTrainSchedulesQueryDto) { return this.trainSchedulingService.getContainerTrainSchedules(query); } @Get("container/schedules/:id") @TrainSchedulingView() @ApiOperation({ summary: "Get container train schedule detail" }) getContainerTrainScheduleById(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getContainerTrainScheduleById(id); } @Get("bulk/schedules/:id") @TrainSchedulingView() @ApiOperation({ summary: "Get bulk train schedule detail" }) getBulkTrainScheduleById(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getContainerTrainScheduleById(id); } @Post("container/schedules/:id/cancel") @TrainSchedulingManage() @ApiOperation({ summary: "Cancel container train schedule" }) cancelTrainSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.cancelTrainSchedule(id); } @Post('bulk/schedules/:id/cancel') @TrainSchedulingManage() @ApiOperation({ summary: "Cancel bulk train schedule" }) cancelBulkTrainSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.cancelTrainSchedule(id); } }