import 'reflect-metadata'; import { config } from 'dotenv'; import { resolve } from 'path'; import { WagonStatus } from '@edr/types'; config({ path: resolve(__dirname, '../../.env') }); import { AppDataSource } from '../data-source'; import { BookingContainer } from '../modules/bookings/entities/booking-container.entity'; import { Booking } from '../modules/bookings/entities/booking.entity'; import { Company, CompanyStatus, CompanyType } from '../modules/companies/entities/company.entity'; import { Locomotive } from '../modules/locomotives/entities/locomotive.entity'; import { ImportDjiboutiOperation } from '../modules/train-scheduling/entities/import-djibouti-operation.entity'; import { ContainerType } from '../modules/rule-engine/entities/container-type.entity'; import { ServiceType } from '../modules/rule-engine/entities/service-type.entity'; import { Yard } from '../modules/rule-engine/entities/yard.entity'; import { TrainScheduleBooking } from '../modules/train-schedules/entities/train-schedule-booking.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 { 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'; const TRAIN_NUMBER = 'NEGAD-INDODE-ARR-01'; const BOOKING_REFS = ['NEGAD-INDODE-BKG-001', 'NEGAD-INDODE-BKG-002', 'NEGAD-INDODE-BKG-003'] as const; function addHours(date: Date, hours: number): Date { return new Date(date.getTime() + hours * 60 * 60 * 1000); } async function main() { const dataSource = await AppDataSource.initialize(); try { await dataSource.transaction(async (manager) => { const yardRepo = manager.getRepository(Yard); const locomotiveRepo = manager.getRepository(Locomotive); const trainSetRepo = manager.getRepository(TrainSet); const scheduleRepo = manager.getRepository(TrainSchedule); const wagonTypeRepo = manager.getRepository(WagonType); const wagonRepo = manager.getRepository(Wagon); const trainSetWagonRepo = manager.getRepository(TrainSetWagon); const serviceTypeRepo = manager.getRepository(ServiceType); const containerTypeRepo = manager.getRepository(ContainerType); const companyRepo = manager.getRepository(Company); const bookingRepo = manager.getRepository(Booking); const bookingContainerRepo = manager.getRepository(BookingContainer); const scheduleBookingRepo = manager.getRepository(TrainScheduleBooking); const allocationRepo = manager.getRepository(WagonBookingAllocation); const containerItemRepo = manager.getRepository(WagonAllocationContainerItem); const importOperationRepo = manager.getRepository(ImportDjiboutiOperation); const negad = (await yardRepo.findOne({ where: { code: 'NEGAD' } })) ?? (await yardRepo.save( yardRepo.create({ code: 'NEGAD', label: 'Negad / Nagad', country: 'Djibouti', isActive: true, displayOrder: 5, }), )); if (negad.label !== 'Negad / Nagad') { negad.label = 'Negad / Nagad'; await yardRepo.save(negad); } const indode = (await yardRepo.findOne({ where: { code: 'INDODE' } })) ?? (await yardRepo.save( yardRepo.create({ code: 'INDODE', label: 'Indode Dry Port', country: 'Ethiopia', isActive: true, displayOrder: 6, }), )); const locomotive = (await locomotiveRepo.findOne({ where: { code: 'NEGAD-INDODE-LOCO' } })) ?? (await locomotiveRepo.save( locomotiveRepo.create({ code: 'NEGAD-INDODE-LOCO', name: 'Negad to Indode Demo Locomotive', locomotiveType: 'DIESEL', maxPullWeightTons: 4200, maxTrainLengthMeters: 760, status: 'AVAILABLE', currentYardId: indode.id, }), )); const wagonType = (await wagonTypeRepo.findOne({ where: { code: 'NEGAD-FLAT' } })) ?? (await wagonTypeRepo.save( wagonTypeRepo.create({ code: 'NEGAD-FLAT', name: 'Negad Demo Flat Wagon', capacityTons: 70, lengthMeters: 14, supportedLoadTypes: ['CONTAINER'], isActive: true, equatedLengthM: 14, tareWeightTons: 20, supportsContainer: true, maxContainerGrossT: 70, }), )); const containerType = (await containerTypeRepo.findOne({ where: { code: '40FT' } })) ?? (await containerTypeRepo.save( containerTypeRepo.create({ code: '40FT', label: '40FT', sizeFt: 40, wagonsPerUnit: 1, isReefer: false, isOpenTop: false, isActive: true, displayOrder: 2, }), )); const serviceType = (await serviceTypeRepo.findOne({ where: { code: 'RAIL_CONTAINER' } })) ?? (await serviceTypeRepo.save( serviceTypeRepo.create({ code: 'RAIL_CONTAINER', serviceName: 'Rail Container Service', description: 'Rail container service for demo marshalling', canBeBookedAlone: true, includesFirstMile: false, includesLastMile: false, includesCustoms: false, isActive: true, displayOrder: 1, }), )); const company = (await companyRepo.findOne({ where: { tin: 'NEGADIND01' } })) ?? (await companyRepo.save( companyRepo.create({ name: 'Negad Indode Marshalling Demo Customer', type: CompanyType.Customer, status: CompanyStatus.Active, tin: 'NEGADIND01', vatNumber: 'NEGADIND01', fanNumber: 'NEGADINDODE00001', country: 'Ethiopia', address: 'Indode Dry Port', phone: '251900000202', email: 'negad-indode-demo@edr.local', contactPersonName: 'Marshalling Demo', contactPersonPhone: '251900000202', generalManagerName: 'Demo Manager', generalManagerEmail: 'negad-indode-demo@edr.local', generalManagerPhone: '251900000202', }), )); const now = new Date(); const departure = addHours(now, -12); const arrival = now; let schedule = await scheduleRepo.findOne({ where: { trainNumber: TRAIN_NUMBER } }); if (!schedule) { const trainSet = await trainSetRepo.save( trainSetRepo.create({ locomotiveId: locomotive.id, totalWeightTons: 960, totalLengthMeters: 420, wagonCount: BOOKING_REFS.length, status: 'COMPLETED', }), ); schedule = scheduleRepo.create({ trainSetId: trainSet.id, originStationId: negad.id, destinationStationId: indode.id, scheduledDepartureDate: departure, scheduledArrivalDate: arrival, actualDepartureAt: departure, actualArrivalAt: arrival, status: 'ARRIVED' as TrainSchedule['status'], trainNumber: TRAIN_NUMBER, direction: 'IMPORT', maxWagons: 53, bookingWindowStatus: 'CLOSED', }); } else { schedule.originStationId = negad.id; schedule.destinationStationId = indode.id; schedule.scheduledDepartureDate = departure; schedule.scheduledArrivalDate = arrival; schedule.actualDepartureAt = departure; schedule.actualArrivalAt = arrival; schedule.status = 'ARRIVED' as TrainSchedule['status']; schedule.direction = 'IMPORT'; schedule.bookingWindowStatus = 'CLOSED'; if (schedule.trainSetId) { await trainSetRepo.update(schedule.trainSetId, { status: 'COMPLETED' }); } } const saved = await scheduleRepo.save(schedule); await trainSetRepo.update(saved.trainSetId, { totalWeightTons: BOOKING_REFS.length * 28, totalLengthMeters: BOOKING_REFS.length * 14, wagonCount: BOOKING_REFS.length, status: 'COMPLETED', }); const existingSlots = await trainSetWagonRepo.find({ where: { trainSetId: saved.trainSetId } }); const existingAllocations = existingSlots.length ? await allocationRepo.find({ where: existingSlots.map((slot) => ({ trainSetWagonId: slot.id })), }) : []; if (existingAllocations.length) { await containerItemRepo.delete( existingAllocations.map((allocation) => ({ wagonBookingAllocationId: allocation.id })), ); } if (existingSlots.length) { await allocationRepo.delete(existingSlots.map((slot) => ({ trainSetWagonId: slot.id }))); await wagonRepo.update( existingSlots.map((slot) => ({ trainSetWagonId: slot.id })), { trainSetWagonId: null, currentTrainScheduleId: null, sequenceNumber: null, status: WagonStatus.Available, }, ); await trainSetWagonRepo.delete({ trainSetId: saved.trainSetId }); } for (const [index, reference] of BOOKING_REFS.entries()) { const sequenceNo = index + 1; const containerNumber = `NEGADIND${String(sequenceNo).padStart(4, '0')}`; const weightTons = 26 + sequenceNo; let booking = await bookingRepo.findOne({ where: { reference } }); if (!booking) { booking = bookingRepo.create({ reference }); } Object.assign(booking, { companyId: company.id, originYardId: negad.id, destinationYardId: indode.id, serviceTypeId: serviceType.id, status: 'IN_TRANSIT', paymentStatus: 'PAID', scheduledDate: departure, estimatedShipmentDate: departure, contractType: 'SPOT', equipmentReturn: 'TERMINAL', paymentCurrency: 'ETB', totalAmount: 0, isGovernment: false, tradeDirection: 'IMPORT', freightType: 'CONTAINER', cargoTypeId: null, cargoFreeText: `Negad to Indode demo container ${sequenceNo}`, cargoTotalWeightVgm: weightTons, priorityScore: 75 - index, trainScheduleId: saved.id, schedulingStatus: 'SCHEDULED', scheduledAt: now, wagonsRequired: 1, }); booking = await bookingRepo.save(booking); await bookingContainerRepo.delete({ bookingId: booking.id }); const bookingContainer = await bookingContainerRepo.save( bookingContainerRepo.create({ bookingId: booking.id, containerTypeId: containerType.id, containerNumber, quantity: 1, vgmPerUnitTons: weightTons, totalVgmTons: weightTons, wagonsRequired: 1, weightLimitRuleId: null, isOverweight: false, overweightExcessTons: null, }), ); await scheduleBookingRepo.upsert( { trainScheduleId: saved.id, bookingId: booking.id }, { conflictPaths: { trainScheduleId: true, bookingId: true } }, ); const wagon = await wagonRepo.save( wagonRepo.create({ wagonNumber: `NEGAD-INDODE-WGN-${String(sequenceNo).padStart(2, '0')}`, wagonTypeId: wagonType.id, trainId: null, sequenceNumber: sequenceNo, status: WagonStatus.Assigned, currentYardId: indode.id, notes: 'Demo wagon for Negad to Indode marshalling', trainSetWagonId: null, currentTrainScheduleId: saved.id, }), ); const trainSetWagon = await trainSetWagonRepo.save( trainSetWagonRepo.create({ trainSetId: saved.trainSetId, wagonTypeId: wagonType.id, physicalWagonId: wagon.id, sequenceNo, capacityTons: 70, lengthMeters: 14, assignedWeightTons: weightTons, status: 'LOADED', }), ); await wagonRepo.update(wagon.id, { trainSetWagonId: trainSetWagon.id }); const allocation = await allocationRepo.save( allocationRepo.create({ trainSetWagonId: trainSetWagon.id, bookingId: booking.id, allocatedWeightTons: weightTons, loadType: 'CONTAINER', status: 'LOADED', confirmedAt: now, }), ); await containerItemRepo.save( containerItemRepo.create({ wagonBookingAllocationId: allocation.id, bookingContainerId: bookingContainer.id, containerId: null, containerNumber, containerTypeId: containerType.id, positionOnWagon: 1, sealNumber: `SEAL-${containerNumber}`, grossWeightTons: weightTons, }), ); } await importOperationRepo.upsert( { trainScheduleId: saved.id, documents: {}, gatepassGrantedAt: departure, readyForLoadingAt: departure, loadedOnTrainAt: departure, departedFromDjiboutiAt: departure, loadListGeneratedAt: now, performedBy: 'Seed Demo', notes: 'Seeded marshalling data for Negad to Indode arrived train', }, { conflictPaths: { trainScheduleId: true } }, ); console.log(`Seeded ARRIVED train ${TRAIN_NUMBER}`); console.log(`Schedule ID: ${saved.id}`); console.log(`Route: ${negad.code} -> ${indode.code}`); console.log(`Marshalling data: ${BOOKING_REFS.length} bookings, wagons and allocations`); }); } finally { await dataSource.destroy(); } } main().catch((err) => { console.error('Negad to Indode arrived train seed failed:', err); process.exit(1); });