import 'reflect-metadata'; import { config } from 'dotenv'; import { resolve } from 'path'; import { WagonStatus } from '@edr/types'; import { In } from 'typeorm'; 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 { CompanyProfile, ProfileStatus, ProfileType } from '../modules/companies/entities/company-profile.entity'; import { Company, CompanyKind, CompanyStatus, CompanyType } from '../modules/companies/entities/company.entity'; import { Container } from '../modules/container-management/entities/container.entity'; import { Locomotive } from '../modules/locomotives/entities/locomotive.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 { ImportDjiboutiOperation } from '../modules/train-scheduling/entities/import-djibouti-operation.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'; 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 Direction = 'IMPORT' | 'EXPORT'; type TrainStatus = 'SCHEDULED' | 'ARRIVED'; interface ScenarioTrain { trainNumber: string; direction: Direction; status: TrainStatus; departureOffsetHours: number; arrivalOffsetHours: number; bookings: Array<{ reference: string; mileVariant: 'FIRST_MILE' | 'LAST_MILE' | 'TERMINAL'; containerNumber: string; weightTons: number; }>; } const SCENARIOS: ScenarioTrain[] = [ { trainNumber: 'GP-IMP-ARR-01', direction: 'IMPORT', status: 'ARRIVED', departureOffsetHours: -18, arrivalOffsetHours: -6, bookings: [ { reference: 'GP-IMP-ARR-LM-001', mileVariant: 'LAST_MILE', containerNumber: 'GPIM0000001', weightTons: 22 }, { reference: 'GP-IMP-ARR-TM-002', mileVariant: 'TERMINAL', containerNumber: 'GPIM0000002', weightTons: 24 }, ], }, { trainNumber: 'GP-IMP-NARR-01', direction: 'IMPORT', status: 'SCHEDULED', departureOffsetHours: 6, arrivalOffsetHours: 18, bookings: [ { reference: 'GP-IMP-NARR-LM-001', mileVariant: 'LAST_MILE', containerNumber: 'GPIM0000003', weightTons: 21 }, { reference: 'GP-IMP-NARR-TM-002', mileVariant: 'TERMINAL', containerNumber: 'GPIM0000004', weightTons: 23 }, ], }, { trainNumber: 'GP-EXP-ARR-01', direction: 'EXPORT', status: 'ARRIVED', departureOffsetHours: -16, arrivalOffsetHours: -4, bookings: [ { reference: 'GP-EXP-ARR-FM-001', mileVariant: 'FIRST_MILE', containerNumber: 'GPEX0000001', weightTons: 20 }, { reference: 'GP-EXP-ARR-TM-002', mileVariant: 'TERMINAL', containerNumber: 'GPEX0000002', weightTons: 22 }, ], }, { trainNumber: 'GP-EXP-NARR-01', direction: 'EXPORT', status: 'SCHEDULED', departureOffsetHours: 8, arrivalOffsetHours: 20, bookings: [ { reference: 'GP-EXP-NARR-FM-001', mileVariant: 'FIRST_MILE', containerNumber: 'GPEX0000003', weightTons: 19 }, { reference: 'GP-EXP-NARR-TM-002', mileVariant: 'TERMINAL', containerNumber: 'GPEX0000004', weightTons: 21 }, ], }, ]; const addHours = (date: Date, hours: number): Date => new Date(date.getTime() + hours * 60 * 60 * 1000); async function main() { const dataSource = await AppDataSource.initialize(); try { const seeded = await dataSource.transaction(async (manager) => { if (await isAlreadySeeded(manager)) { return null; } const refs = await ensureReferences(manager); const now = new Date(); const result: Array<{ trainNumber: string; bookings: string[] }> = []; for (const scenario of SCENARIOS) { const schedule = await seedScenarioTrain(manager, scenario, refs, now); result.push({ trainNumber: schedule.trainNumber ?? scenario.trainNumber, bookings: scenario.bookings.map((booking) => booking.reference), }); } return result; }); console.log('Gate-pass train scenario seed complete.'); if (seeded) { for (const row of seeded) { console.log(`${row.trainNumber}: ${row.bookings.join(', ')}`); } } else { console.log('Gate-pass train scenarios already seeded; nothing changed.'); } } finally { await dataSource.destroy(); } } async function isAlreadySeeded(manager: any): Promise { const scheduleRepo = manager.getRepository(TrainSchedule); const bookingRepo = manager.getRepository(Booking); const trainNumbers = SCENARIOS.map((scenario) => scenario.trainNumber); const bookingRefs = SCENARIOS.flatMap((scenario) => scenario.bookings.map((booking) => booking.reference)); const [scheduleCount, bookingCount] = await Promise.all([ scheduleRepo.count({ where: { trainNumber: In(trainNumbers) } }), bookingRepo.count({ where: { reference: In(bookingRefs) } }), ]); return scheduleCount === trainNumbers.length && bookingCount === bookingRefs.length; } async function ensureReferences(manager: any) { const yardRepo = manager.getRepository(Yard); const serviceTypeRepo = manager.getRepository(ServiceType); const containerTypeRepo = manager.getRepository(ContainerType); const wagonTypeRepo = manager.getRepository(WagonType); const companyRepo = manager.getRepository(Company); const profileRepo = manager.getRepository(CompanyProfile); const warehouseRepo = manager.getRepository(Warehouse); const warehouseYardRepo = manager.getRepository(WarehouseYard); const warehouseZoneRepo = manager.getRepository(WarehouseZone); const djiboutiYard = (await yardRepo.findOne({ where: { code: 'NAGAD' } })) ?? (await yardRepo.findOne({ where: { code: 'DJIB_PORT' } })) ?? (await yardRepo.findOne({ where: { country: 'Djibouti' } })) ?? (await yardRepo.save( yardRepo.create({ code: 'NAGAD', label: 'Nagad Port', country: 'Djibouti', isActive: true, displayOrder: 90, }), )); const ethiopiaYard = (await yardRepo.findOne({ where: { code: 'INDODE' } })) ?? (await yardRepo.findOne({ where: { code: 'MOJO' } })) ?? (await yardRepo.findOne({ where: { country: 'Ethiopia' } })) ?? (await yardRepo.save( yardRepo.create({ code: 'INDODE', label: 'Indode Dry Port', country: 'Ethiopia', isActive: true, displayOrder: 91, }), )); 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 gate-pass scenario seed', canBeBookedAlone: true, includesFirstMile: false, includesLastMile: false, includesCustoms: false, isActive: true, displayOrder: 1, }), )); const containerType = (await containerTypeRepo.findOne({ where: { code: '40FT' } })) ?? (await containerTypeRepo.findOne({ where: { isActive: true } })) ?? (await containerTypeRepo.save( containerTypeRepo.create({ code: '40FT', label: '40FT', sizeFt: 40, isReefer: false, isOpenTop: false, isActive: true, displayOrder: 1, }), )); const wagonType = (await wagonTypeRepo.findOne({ where: { code: 'GP-FLAT' } })) ?? (await wagonTypeRepo.findOne({ where: { supportsContainer: true } })) ?? (await wagonTypeRepo.findOne({ where: { isActive: true } })) ?? (await wagonTypeRepo.save( wagonTypeRepo.create({ code: 'GP-FLAT', name: 'Gate Pass Demo Flat Wagon', capacityTons: 70, lengthMeters: 14, supportedLoadTypes: ['CONTAINER'], isActive: true, equatedLengthM: 14, tareWeightTons: 20, supportsContainer: true, maxContainerGrossT: 70, }), )); const company = (await companyRepo.findOne({ where: { tin: 'GTPASS001' } })) ?? (await companyRepo.save( companyRepo.create({ name: 'Gate Pass Scenario Customer', type: CompanyType.Customer, kind: CompanyKind.Commercial, status: CompanyStatus.Active, tin: 'GTPASS001', vatNumber: 'GTPASS001', fanNumber: 'GTPASS0000001', country: 'Ethiopia', address: 'Indode Dry Port', phone: '251900000555', email: 'gate-pass-scenarios@edr.local', contactPersonName: 'Gate Pass Tester', contactPersonPhone: '251900000555', }), )); const importerProfile = await ensureProfile(profileRepo, company.id, ProfileType.importer, 'GP-IMP'); const exporterProfile = await ensureProfile(profileRepo, company.id, ProfileType.exporter, 'GP-EXP'); const warehouse = (await warehouseRepo.findOne({ where: { code: 'INDODE_OPEN' } })) ?? (await warehouseRepo.findOne({ where: {} })); if (!warehouse) { throw new Error('No warehouse found. Run the Indode/warehouse seed before gate-pass scenarios.'); } const warehouseYard = await warehouseYardRepo.findOne({ where: { warehouseId: warehouse.id } }); if (!warehouseYard) { throw new Error(`No warehouse yard found for ${warehouse.code ?? warehouse.id}.`); } const warehouseZone = await warehouseZoneRepo.findOne({ where: { yardId: warehouseYard.id } }); if (!warehouseZone) { throw new Error(`No warehouse zone found for yard ${warehouseYard.id}.`); } return { djiboutiYard, ethiopiaYard, serviceType, containerType, wagonType, company, importerProfile, exporterProfile, warehouse, warehouseYard, warehouseZone, }; } async function ensureProfile(repo: any, companyId: string, type: ProfileType, reference: string): Promise { const existing = await repo.findOne({ where: { companyId, type } }); if (existing) return existing; return repo.save( repo.create({ companyId, type, reference, status: ProfileStatus.Active, businessLicense: `${reference}-LICENSE`, }), ); } async function seedScenarioTrain(manager: any, scenario: ScenarioTrain, refs: Awaited>, now: Date) { const locomotiveRepo = manager.getRepository(Locomotive); const trainSetRepo = manager.getRepository(TrainSet); const scheduleRepo = manager.getRepository(TrainSchedule); const trainSetWagonRepo = manager.getRepository(TrainSetWagon); const wagonRepo = manager.getRepository(Wagon); const departure = addHours(now, scenario.departureOffsetHours); const arrival = addHours(now, scenario.arrivalOffsetHours); const isArrived = scenario.status === 'ARRIVED'; const originYard = scenario.direction === 'IMPORT' ? refs.djiboutiYard : refs.ethiopiaYard; const destinationYard = scenario.direction === 'IMPORT' ? refs.ethiopiaYard : refs.djiboutiYard; const totalWeightTons = scenario.bookings.reduce((sum, booking) => sum + booking.weightTons, 0); const locomotive = (await locomotiveRepo.findOne({ where: { code: 'GP-DEMO-LOCO' } })) ?? (await locomotiveRepo.save( locomotiveRepo.create({ code: 'GP-DEMO-LOCO', name: 'Gate Pass Scenario Locomotive', locomotiveType: 'DIESEL', maxPullWeightTons: 4200, maxTrainLengthMeters: 760, status: 'AVAILABLE', currentYardId: originYard.id, }), )); let schedule = await scheduleRepo.findOne({ where: { trainNumber: scenario.trainNumber } }); let trainSet: TrainSet | null = schedule?.trainSetId ? await trainSetRepo.findOne({ where: { id: schedule.trainSetId } }) : null; if (!trainSet) { trainSet = await trainSetRepo.save( trainSetRepo.create({ locomotiveId: locomotive.id, totalWeightTons, totalLengthMeters: scenario.bookings.length * 14, wagonCount: scenario.bookings.length, status: isArrived ? 'COMPLETED' : 'ASSIGNED', }), ); } else { await trainSetRepo.update(trainSet.id, { locomotiveId: locomotive.id, totalWeightTons, totalLengthMeters: scenario.bookings.length * 14, wagonCount: scenario.bookings.length, status: isArrived ? 'COMPLETED' : 'ASSIGNED', }); } if (!trainSet) { throw new Error(`Could not create train set for ${scenario.trainNumber}`); } const trainSetId = trainSet.id; if (!schedule) { schedule = scheduleRepo.create({ trainNumber: scenario.trainNumber }); } Object.assign(schedule, { trainSetId, originStationId: originYard.id, destinationStationId: destinationYard.id, scheduledDepartureDate: departure, scheduledArrivalDate: arrival, actualDepartureAt: isArrived ? departure : null, actualArrivalAt: isArrived ? arrival : null, status: scenario.status, direction: scenario.direction, maxWagons: 53, bookingWindowStatus: 'CLOSED', }); schedule = await scheduleRepo.save(schedule); for (const [index, bookingSpec] of scenario.bookings.entries()) { const sequenceNo = index + 1; const wagon = await ensureWagon(manager, scenario, sequenceNo, refs.wagonType.id, originYard.id, schedule.id); const trainSetWagon = await ensureTrainSetWagon( trainSetWagonRepo, trainSetId, refs.wagonType.id, wagon.id, sequenceNo, bookingSpec.weightTons, isArrived, ); await wagonRepo.update(wagon.id, { trainSetWagonId: trainSetWagon.id }); const booking = await ensureBooking(manager, scenario, bookingSpec, refs, departure, now, schedule.id); const bookingContainer = await ensureBookingContainer(manager, booking.id, refs.containerType.id, bookingSpec); const allocation = await ensureAllocation(manager, trainSetWagon.id, booking.id, bookingSpec.weightTons, isArrived, now); const container = await ensureContainer(manager, booking.id, bookingContainer.id, allocation.id, wagon.id, sequenceNo, bookingSpec, refs.containerType.id, isArrived); await ensureContainerItem(manager, allocation.id, bookingContainer.id, container.id, refs.containerType.id, sequenceNo, bookingSpec); await ensureScheduleBooking(manager, schedule.id, booking.id); if (scenario.direction === 'EXPORT') { await ensureExportInventory(manager, refs, booking.id, container.id, bookingSpec.weightTons, isArrived, now); } } if (scenario.direction === 'IMPORT') { await ensureImportOperation(manager, schedule.id, scenario, departure, isArrived); } return schedule; } async function ensureWagon(manager: any, scenario: ScenarioTrain, sequenceNo: number, wagonTypeId: string, yardId: string, scheduleId: string): Promise { const repo = manager.getRepository(Wagon); const wagonNumber = `${scenario.trainNumber}-W${String(sequenceNo).padStart(2, '0')}`; const existing = await repo.findOne({ where: { wagonNumber } }); const values = { wagonNumber, wagonTypeId, trainId: null, sequenceNumber: sequenceNo, status: WagonStatus.Assigned, currentYardId: yardId, currentTrainScheduleId: scheduleId, notes: 'Gate-pass scenario seed wagon', }; return repo.save(repo.create({ ...(existing ?? {}), ...values })); } async function ensureTrainSetWagon(repo: any, trainSetId: string, wagonTypeId: string, wagonId: string, sequenceNo: number, weightTons: number, isArrived: boolean): Promise { const existing = await repo.findOne({ where: { trainSetId, sequenceNo } }); return repo.save( repo.create({ ...(existing ?? {}), trainSetId, wagonTypeId, physicalWagonId: wagonId, sequenceNo, capacityTons: 70, lengthMeters: 14, assignedWeightTons: weightTons, status: isArrived ? 'DEPARTED' : 'LOADED', }), ); } async function ensureBooking(manager: any, scenario: ScenarioTrain, bookingSpec: ScenarioTrain['bookings'][number], refs: Awaited>, departure: Date, now: Date, scheduleId: string): Promise { const repo = manager.getRepository(Booking); const originYard = scenario.direction === 'IMPORT' ? refs.djiboutiYard : refs.ethiopiaYard; const destinationYard = scenario.direction === 'IMPORT' ? refs.ethiopiaYard : refs.djiboutiYard; const existing = await repo.findOne({ where: { reference: bookingSpec.reference } }); const profile = scenario.direction === 'IMPORT' ? refs.importerProfile : refs.exporterProfile; const hasFirstMile = bookingSpec.mileVariant === 'FIRST_MILE'; const hasLastMile = bookingSpec.mileVariant === 'LAST_MILE'; return repo.save( repo.create({ ...(existing ?? {}), reference: bookingSpec.reference, companyId: refs.company.id, companyProfileId: profile.id, originYardId: originYard.id, destinationYardId: destinationYard.id, serviceTypeId: refs.serviceType.id, status: scenario.status === 'ARRIVED' ? 'IN_TRANSIT' : 'PAID', paymentStatus: 'PAID', scheduledDate: departure, estimatedShipmentDate: departure, contractType: 'SPOT', equipmentReturn: 'TERMINAL', paymentCurrency: 'ETB', totalAmount: 0, isGovernment: false, tradeDirection: scenario.direction, freightType: 'CONTAINER', cargoTypeId: null, cargoFreeText: `${scenario.direction} gate-pass scenario ${bookingSpec.mileVariant.toLowerCase().replace('_', ' ')}`, cargoTotalWeightVgm: bookingSpec.weightTons * 1000, firstMilePickupAddress: hasFirstMile ? 'Customer factory pickup - Addis Ababa' : null, firstMilePickupLat: hasFirstMile ? 9.03 : null, firstMilePickupLng: hasFirstMile ? 38.74 : null, lastMileDeliveryAddress: hasLastMile ? 'Customer warehouse delivery - Addis Ababa' : null, lastMileDeliveryLat: hasLastMile ? 8.98 : null, lastMileDeliveryLng: hasLastMile ? 38.8 : null, trainScheduleId: scheduleId, schedulingStatus: scenario.status === 'ARRIVED' ? 'DISPATCHED' : 'SCHEDULED', scheduledAt: now, wagonsRequired: 1, }), ); } async function ensureBookingContainer(manager: any, bookingId: string, containerTypeId: string, bookingSpec: ScenarioTrain['bookings'][number]): Promise { const repo = manager.getRepository(BookingContainer); const existing = await repo.findOne({ where: { bookingId } }); return repo.save( repo.create({ ...(existing ?? {}), bookingId, containerTypeId, containerNumber: bookingSpec.containerNumber, containerSize: '40', quantity: 1, hazardousQuantity: 0, reeferQuantity: 0, vgmPerUnitTons: bookingSpec.weightTons, totalVgmTons: bookingSpec.weightTons, wagonsRequired: 1, weightLimitRuleId: null, isOverweight: false, overweightExcessTons: null, }), ); } async function ensureAllocation(manager: any, trainSetWagonId: string, bookingId: string, weightTons: number, isArrived: boolean, now: Date): Promise { const repo = manager.getRepository(WagonBookingAllocation); const existing = await repo.findOne({ where: { trainSetWagonId, bookingId } }); return repo.save( repo.create({ ...(existing ?? {}), trainSetWagonId, bookingId, allocatedWeightTons: weightTons, loadType: 'CONTAINER', status: isArrived ? 'DEPARTED' : 'LOADED', confirmedAt: now, }), ); } async function ensureContainer(manager: any, bookingId: string, bookingContainerId: string, allocationId: string, wagonId: string, position: number, bookingSpec: ScenarioTrain['bookings'][number], containerTypeId: string, isArrived: boolean): Promise { const repo = manager.getRepository(Container); const existing = await repo.findOne({ where: { containerNumber: bookingSpec.containerNumber } }); return repo.save( repo.create({ ...(existing ?? {}), containerNumber: bookingSpec.containerNumber, containerTypeId, wagonId, position, tareWeight: 3800, maxGrossWeight: 30480, sealNumber: `SEAL-${bookingSpec.containerNumber}`, status: isArrived ? 'IN_TRANSIT' : 'LOADED', bookingId, wagonBookingAllocationId: allocationId, bookingContainerId, }), ); } async function ensureContainerItem(manager: any, allocationId: string, bookingContainerId: string, containerId: string, containerTypeId: string, position: number, bookingSpec: ScenarioTrain['bookings'][number]): Promise { const repo = manager.getRepository(WagonAllocationContainerItem); await repo.delete({ wagonBookingAllocationId: allocationId }); await repo.save( repo.create({ wagonBookingAllocationId: allocationId, bookingContainerId, containerId, containerNumber: bookingSpec.containerNumber, containerTypeId, positionOnWagon: position, sealNumber: `SEAL-${bookingSpec.containerNumber}`, chassisNumber: `CHS-${bookingSpec.containerNumber}`, grossWeightTons: bookingSpec.weightTons, }), ); } async function ensureScheduleBooking(manager: any, scheduleId: string, bookingId: string): Promise { const repo = manager.getRepository(TrainScheduleBooking); const existing = await repo.findOne({ where: { bookingId } }); await repo.save(repo.create({ ...(existing ?? {}), trainScheduleId: scheduleId, bookingId })); } async function ensureExportInventory(manager: any, refs: Awaited>, bookingId: string, containerId: string, weightTons: number, isArrived: boolean, now: Date): Promise { const repo = manager.getRepository(WarehouseInventory); const existing = await repo.findOne({ where: { bookingId } }); await repo.save( repo.create({ ...(existing ?? {}), warehouseId: refs.warehouse.id, yardId: refs.warehouseYard.id, zoneId: refs.warehouseZone.id, bookingId, containerId, quantity: 1, weight: weightTons * 1000, status: 'LOADED', inspectionStatus: 'PASSED', arrivedAt: addHours(now, -24), inspectedAt: addHours(now, -22), readyForLoadingAt: addHours(now, -20), loadedAt: isArrived ? addHours(now, -16) : null, notes: '[GP-SCENARIO] Export train gate-pass scenario inventory', }), ); } async function ensureImportOperation(manager: any, scheduleId: string, scenario: ScenarioTrain, departure: Date, isArrived: boolean): Promise { const repo = manager.getRepository(ImportDjiboutiOperation); const existing = await repo.findOne({ where: { trainScheduleId: scheduleId } }); await repo.save( repo.create({ ...(existing ?? {}), trainScheduleId: scheduleId, documents: existing?.documents ?? {}, gatepassGrantedAt: null, readyForLoadingAt: null, loadedOnTrainAt: null, departedFromDjiboutiAt: isArrived ? departure : null, loadListGeneratedAt: null, performedBy: 'Gate Pass Scenario Seeder', notes: `[GP-SCENARIO] ${scenario.trainNumber}; fill gate-pass dates during testing`, }), ); } main().catch((error) => { console.error('Gate-pass train scenario seed failed:', error); process.exit(1); });