import { Injectable, Logger } from '@nestjs/common'; import { WagonStatus } from '@edr/types'; import { DataSource } from 'typeorm'; import { Booking } from '../modules/bookings/entities/booking.entity'; import { Company } from '../modules/companies/entities/company.entity'; import { Locomotive } from '../modules/locomotives/entities/locomotive.entity'; import { CargoType } from '../modules/rule-engine/entities/cargo-type.entity'; import { ServiceType } from '../modules/rule-engine/entities/service-type.entity'; import { Yard } from '../modules/rule-engine/entities/yard.entity'; import { ImportDjiboutiOperation } from '../modules/train-scheduling/entities/import-djibouti-operation.entity'; import { WagonAllocationContainerItem } from '../modules/train-schedules/entities/wagon-allocation-container-item.entity'; import { WagonBookingAllocation } from '../modules/train-schedules/entities/wagon-booking-allocation.entity'; import { TrainSchedule } from '../modules/train-schedules/entities/train-schedule.entity'; import { TrainScheduleBooking } from '../modules/train-schedules/entities/train-schedule-booking.entity'; import { TrainSetWagon } from '../modules/train-sets/entities/train-set-wagon.entity'; import { TrainSet } from '../modules/train-sets/entities/train-set.entity'; import { WagonType } from '../modules/wagon-types/entities/wagon-type.entity'; import { Wagon } from '../modules/wagons/entities/wagon.entity'; import { WarehouseInventory } from '../modules/warehouses/entities/warehouse-inventory.entity'; import { WarehouseYard } from '../modules/warehouses/entities/warehouse-yard.entity'; import { WarehouseZone } from '../modules/warehouses/entities/warehouse-zone.entity'; import { Warehouse } from '../modules/warehouses/entities/warehouse.entity'; type DemoDirection = 'IMPORT' | 'EXPORT'; interface DemoTrain { trainNumber: string; direction: DemoDirection; status: 'SCHEDULED' | 'DISPATCHED' | 'ARRIVED'; bookingPrefix: string; departureOffsetHours: number; } const DEMO_TRAINS: DemoTrain[] = [ { trainNumber: 'MSH-DEMO-IMP-01', direction: 'IMPORT', status: 'SCHEDULED', bookingPrefix: 'MSH-IMP-01', departureOffsetHours: 6, }, { trainNumber: 'MSH-DEMO-IMP-02', direction: 'IMPORT', status: 'DISPATCHED', bookingPrefix: 'MSH-IMP-02', departureOffsetHours: -3, }, { trainNumber: 'MSH-DEMO-IMP-03', direction: 'IMPORT', status: 'ARRIVED', bookingPrefix: 'MSH-IMP-03', departureOffsetHours: -14, }, { trainNumber: 'MSH-DEMO-EXP-01', direction: 'EXPORT', status: 'SCHEDULED', bookingPrefix: 'MSH-EXP-01', departureOffsetHours: 8, }, { trainNumber: 'MSH-DEMO-EXP-02', direction: 'EXPORT', status: 'DISPATCHED', bookingPrefix: 'MSH-EXP-02', departureOffsetHours: -2, }, { trainNumber: 'MSH-DEMO-EXP-03', direction: 'EXPORT', status: 'ARRIVED', bookingPrefix: 'MSH-EXP-03', departureOffsetHours: -12, }, ]; @Injectable() export class MarshallingDemoTrainsSeeder { private readonly logger = new Logger(MarshallingDemoTrainsSeeder.name); constructor(private readonly dataSource: DataSource) {} async run(): Promise { try { const yardRepo = this.dataSource.getRepository(Yard); const serviceTypeRepo = this.dataSource.getRepository(ServiceType); const cargoTypeRepo = this.dataSource.getRepository(CargoType); const wagonTypeRepo = this.dataSource.getRepository(WagonType); const warehouseRepo = this.dataSource.getRepository(Warehouse); const warehouseYardRepo = this.dataSource.getRepository(WarehouseYard); const warehouseZoneRepo = this.dataSource.getRepository(WarehouseZone); const djiboutiYard = (await yardRepo.findOne({ where: { code: 'NAGAD' } })) ?? (await yardRepo.findOne({ where: { code: 'DJIB_PORT' } })) ?? (await yardRepo.findOne({ where: { country: 'Djibouti' } })); const ethiopiaYard = (await yardRepo.findOne({ where: { code: 'INDODE' } })) ?? (await yardRepo.findOne({ where: { code: 'MOJO' } })) ?? (await yardRepo.findOne({ where: { country: 'Ethiopia' } })); const serviceType = (await serviceTypeRepo.findOne({ where: { code: 'RAIL_CONTAINER' } })) ?? (await serviceTypeRepo.findOne({ where: { isActive: true } })); const cargoType = await cargoTypeRepo.findOne({ where: { isActive: true } }); const wagonType = (await wagonTypeRepo.findOne({ where: { code: 'NW5' } })) ?? (await wagonTypeRepo.findOne({ where: { supportsContainer: true } })) ?? (await wagonTypeRepo.findOne({ where: { isActive: true } })); const warehouse = await warehouseRepo.findOne({ where: { code: 'INDODE_OPEN' } }); const warehouseYard = warehouse ? await warehouseYardRepo.findOne({ where: { warehouseId: warehouse.id } }) : null; const warehouseZone = warehouseYard ? await warehouseZoneRepo.findOne({ where: { yardId: warehouseYard.id } }) : null; // bookings.company_id is NOT NULL — reuse any seeded company for the demo. const company = await this.dataSource .getRepository(Company) .findOne({ where: {}, order: { createdAt: 'ASC' } }); const missing = [ !djiboutiYard ? 'Djibouti yard' : '', !ethiopiaYard ? 'Ethiopia yard' : '', !serviceType ? 'service type' : '', !wagonType ? 'wagon type' : '', !warehouse ? 'INDODE_OPEN warehouse' : '', !warehouseYard ? 'warehouse yard' : '', !warehouseZone ? 'warehouse zone' : '', !company ? 'company' : '', ].filter(Boolean); if (missing.length) { this.logger.warn(`Cannot seed marshalling demo trains, missing: ${missing.join(', ')}`); return; } let seeded = 0; for (const demo of DEMO_TRAINS) { const created = await this.seedTrain(demo, { djiboutiYard: djiboutiYard!, ethiopiaYard: ethiopiaYard!, serviceType: serviceType!, cargoType, wagonType: wagonType!, warehouse: warehouse!, warehouseYard: warehouseYard!, warehouseZone: warehouseZone!, company: company!, }); if (created) seeded += 1; } this.logger.log(`Marshalling demo trains ready: ${seeded} new train(s) seeded, 6 total expected`); } catch (error) { this.logger.error( `MarshallingDemoTrainsSeeder failed: ${error instanceof Error ? error.message : String(error)}`, ); } } private async seedTrain( demo: DemoTrain, refs: { djiboutiYard: Yard; ethiopiaYard: Yard; serviceType: ServiceType; cargoType: CargoType | null; wagonType: WagonType; warehouse: Warehouse; warehouseYard: WarehouseYard; warehouseZone: WarehouseZone; company: Company; }, ): Promise { const bookingRepo = this.dataSource.getRepository(Booking); const trainSetRepo = this.dataSource.getRepository(TrainSet); const trainSetWagonRepo = this.dataSource.getRepository(TrainSetWagon); const scheduleRepo = this.dataSource.getRepository(TrainSchedule); const scheduleBookingRepo = this.dataSource.getRepository(TrainScheduleBooking); const allocationRepo = this.dataSource.getRepository(WagonBookingAllocation); const containerItemRepo = this.dataSource.getRepository(WagonAllocationContainerItem); const existing = await scheduleRepo.findOne({ where: { trainNumber: demo.trainNumber } }); if (existing) { await this.backfillDispatchQueueInventory(demo, refs); return false; } const now = new Date(); const departure = this.addHours(now, demo.departureOffsetHours); const arrival = this.addHours(departure, demo.direction === 'IMPORT' ? 12 : 10); const isDispatched = demo.status === 'DISPATCHED'; const isArrived = demo.status === 'ARRIVED'; const hasDeparted = isDispatched || isArrived; const originYard = demo.direction === 'IMPORT' ? refs.djiboutiYard : refs.ethiopiaYard; const destinationYard = demo.direction === 'IMPORT' ? refs.ethiopiaYard : refs.djiboutiYard; const locomotive = await this.ensureLocomotive(originYard.id); const bookingWeights = [22.4, 24.8, 18.6, 20.2]; const totalWeight = bookingWeights.reduce((sum, weight) => sum + weight, 0); const wagonCapacity = Number(refs.wagonType.capacityTons) || 70; const wagonLength = Number(refs.wagonType.lengthMeters) || 14; const trainSet = await trainSetRepo.save( trainSetRepo.create({ locomotiveId: locomotive.id, totalWeightTons: totalWeight, totalLengthMeters: wagonLength * bookingWeights.length, wagonCount: bookingWeights.length, status: isArrived ? 'COMPLETED' : isDispatched ? 'DISPATCHED' : 'ASSIGNED', }), ); const schedule = await scheduleRepo.save( scheduleRepo.create({ trainSetId: trainSet.id, originStationId: originYard.id, destinationStationId: destinationYard.id, scheduledDepartureDate: departure, scheduledArrivalDate: arrival, actualDepartureAt: hasDeparted ? departure : null, actualArrivalAt: isArrived ? arrival : null, status: demo.status as TrainSchedule['status'], trainNumber: demo.trainNumber, direction: demo.direction, maxWagons: 53, bookingWindowStatus: 'CLOSED', }), ); for (const [index, weightTons] of bookingWeights.entries()) { const sequence = index + 1; const bookingReference = `${demo.bookingPrefix}-${String(sequence).padStart(3, '0')}`; const containerNumber = `${demo.direction === 'IMPORT' ? 'IMDU' : 'EXPU'}${demo.trainNumber.slice(-2)}${String(sequence).padStart(3, '0')}`; const booking = await bookingRepo.save( bookingRepo.create({ reference: bookingReference, companyId: refs.company.id, originYardId: originYard.id, destinationYardId: destinationYard.id, serviceTypeId: refs.serviceType.id, status: hasDeparted ? 'IN_TRANSIT' : 'PAID', paymentStatus: 'PAID', scheduledDate: departure, contractType: 'SPOT', equipmentReturn: 'TERMINAL', paymentCurrency: 'ETB', totalAmount: 0, isGovernment: false, tradeDirection: demo.direction, freightType: sequence % 2 === 0 ? 'BULK' : 'CONTAINER', cargoTypeId: refs.cargoType?.id ?? null, cargoFreeText: refs.cargoType ? null : `${demo.direction} marshalling demo goods ${sequence}`, cargoTotalWeightVgm: weightTons * 1000, trainScheduleId: schedule.id, schedulingStatus: isArrived ? 'ARRIVED' : isDispatched ? 'DISPATCHED' : 'SCHEDULED', scheduledAt: now, }), ); await this.ensureDispatchQueueInventory({ booking, demo, refs, weightKg: weightTons * 1000, now, }); const physicalWagon = await this.ensureWagon({ wagonNumber: `${demo.trainNumber}-W${String(sequence).padStart(2, '0')}`, wagonTypeId: refs.wagonType.id, yardId: originYard.id, trainScheduleId: schedule.id, dispatched: hasDeparted, }); const trainSetWagon = await trainSetWagonRepo.save( trainSetWagonRepo.create({ trainSetId: trainSet.id, wagonTypeId: refs.wagonType.id, physicalWagonId: physicalWagon.id, sequenceNo: sequence, capacityTons: wagonCapacity, lengthMeters: wagonLength, assignedWeightTons: weightTons, status: hasDeparted ? 'DEPARTED' : 'LOADED', }), ); await this.dataSource.getRepository(Wagon).update(physicalWagon.id, { trainSetWagonId: trainSetWagon.id, }); const allocation = await allocationRepo.save( allocationRepo.create({ trainSetWagonId: trainSetWagon.id, bookingId: booking.id, allocatedWeightTons: weightTons, loadType: booking.freightType === 'CONTAINER' ? 'CONTAINER' : 'BULK', status: hasDeparted ? 'DEPARTED' : 'LOADED', confirmedAt: now, }), ); await containerItemRepo.save( containerItemRepo.create({ wagonBookingAllocationId: allocation.id, containerNumber, positionOnWagon: 1, sealNumber: `SEAL-${demo.trainNumber.slice(-2)}-${sequence}`, chassisNumber: `CHS-${demo.trainNumber.slice(-2)}-${sequence}`, grossWeightTons: weightTons, }), ); await scheduleBookingRepo.save( scheduleBookingRepo.create({ trainScheduleId: schedule.id, bookingId: booking.id, }), ); } if (demo.direction === 'IMPORT') { await this.seedImportOperation(schedule.id, demo.trainNumber, now, departure, hasDeparted); } return true; } private async backfillDispatchQueueInventory( demo: DemoTrain, refs: { warehouse: Warehouse; warehouseYard: WarehouseYard; warehouseZone: WarehouseZone; }, ): Promise { const bookingRepo = this.dataSource.getRepository(Booking); for (let sequence = 1; sequence <= 4; sequence++) { const reference = `${demo.bookingPrefix}-${String(sequence).padStart(3, '0')}`; const booking = await bookingRepo.findOne({ where: { reference } }); if (!booking) continue; await this.ensureDispatchQueueInventory({ booking, demo, refs, weightKg: Number(booking.cargoTotalWeightVgm) || 0, now: new Date(), }); } } private async ensureDispatchQueueInventory(input: { booking: Booking; demo: DemoTrain; refs: { warehouse: Warehouse; warehouseYard: WarehouseYard; warehouseZone: WarehouseZone; }; weightKg: number; now: Date; }): Promise { const repo = this.dataSource.getRepository(WarehouseInventory); const existing = await repo.findOne({ where: { bookingId: input.booking.id } }); if (existing) return; const exportDispatch = input.demo.direction === 'EXPORT'; const arrivedAt = this.addHours(input.now, -8); const inspectedAt = this.addHours(input.now, -6); const readyAt = this.addHours(input.now, -4); const loadedAt = this.addHours(input.now, -2); await repo.save( repo.create({ warehouseId: input.refs.warehouse.id, yardId: input.refs.warehouseYard.id, zoneId: input.refs.warehouseZone.id, bookingId: input.booking.id, quantity: 1, weight: input.weightKg, status: exportDispatch ? 'LOADED' : 'READY_FOR_PICKUP', inspectionStatus: 'PASSED', arrivedAt, unloadedAt: exportDispatch ? null : arrivedAt, inspectedAt, readyForLoadingAt: exportDispatch ? readyAt : null, loadedAt: exportDispatch ? loadedAt : null, readyForPickupAt: exportDispatch ? null : readyAt, notes: `[MSH-DEMO] ${input.demo.trainNumber} dispatch queue test item`, }), ); } private async ensureLocomotive(currentYardId: string): Promise { const repo = this.dataSource.getRepository(Locomotive); const existing = await repo.findOne({ where: { code: 'MSH-DEMO-LOCO' } }); if (existing) return existing; return repo.save( repo.create({ code: 'MSH-DEMO-LOCO', name: 'Marshalling Demo Locomotive', locomotiveType: 'DIESEL', maxPullWeightTons: 4200, maxTrainLengthMeters: 760, status: 'AVAILABLE', currentYardId, }), ); } private async ensureWagon(input: { wagonNumber: string; wagonTypeId: string; yardId: string; trainScheduleId: string; dispatched: boolean; }): Promise { const repo = this.dataSource.getRepository(Wagon); const existing = await repo.findOne({ where: { wagonNumber: input.wagonNumber } }); if (existing) return existing; return repo.save( repo.create({ wagonNumber: input.wagonNumber, wagonTypeId: input.wagonTypeId, currentYardId: input.yardId, currentTrainScheduleId: input.trainScheduleId, status: WagonStatus.Assigned, notes: 'Marshalling demo seed wagon', }), ); } private async seedImportOperation( trainScheduleId: string, trainNumber: string, now: Date, departure: Date, dispatched: boolean, ): Promise { const repo = this.dataSource.getRepository(ImportDjiboutiOperation); await repo.save( repo.create({ trainScheduleId, documents: { DELIVERY_ORDER: this.documentRecord(trainNumber, 'DELIVERY_ORDER', now), PORT_INVOICE: this.documentRecord(trainNumber, 'PORT_INVOICE', now), DJIBOUTI_T1: this.documentRecord(trainNumber, 'DJIBOUTI_T1', now), ETHIOPIA_T1: this.documentRecord(trainNumber, 'ETHIOPIA_T1', now), RAILWAY_BILL: this.documentRecord(trainNumber, 'RAILWAY_BILL', now), }, gatepassGrantedAt: now, readyForLoadingAt: now, loadedOnTrainAt: now, departedFromDjiboutiAt: dispatched ? departure : null, performedBy: 'Marshalling Demo Seeder', notes: '[MSH-DEMO] Import train ready for marshalling document and dispatch workflow', }), ); } private documentRecord(trainNumber: string, type: string, now: Date) { return { reference: `${type}-${trainNumber}`, uploadedAt: now.toISOString(), uploadedBy: 'Marshalling Demo Seeder', notes: 'Seeded document for import marshalling workflow', }; } private addHours(date: Date, hours: number): Date { return new Date(date.getTime() + hours * 60 * 60 * 1000); } }