import { Injectable, Logger } from '@nestjs/common'; import { DataSource } from 'typeorm'; import { Booking } from '../modules/bookings/entities/booking.entity'; import { CargoType } from '../modules/rule-engine/entities/cargo-type.entity'; import { Locomotive } from '../modules/locomotives/entities/locomotive.entity'; import { ServiceType } from '../modules/rule-engine/entities/service-type.entity'; import { Yard } from '../modules/rule-engine/entities/yard.entity'; import { TrainSchedule } from '../modules/train-schedules/entities/train-schedule.entity'; import { TrainScheduleBooking } from '../modules/train-schedules/entities/train-schedule-booking.entity'; import { TrainSet } from '../modules/train-sets/entities/train-set.entity'; import { Warehouse } from '../modules/warehouses/entities/warehouse.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'; /** * One coherent warehouse dataset so EVERY queue/tab shows representative data: * Export → Receive Queue : PAID export bookings, not yet received * Export → Ready To Load : EXPORT inventory READY_FOR_LOADING + inspection PASSED * Export → Loaded/Dispatch : EXPORT inventory LOADED * Import → Arrive Queue : an ARRIVED import train with IN_TRANSIT bookings (no inventory) * Import → Unloaded Queue : UNLOADED import inventory * Import → Dispatch Queue : READY_FOR_PICKUP import inventory (PASSED) * * Idempotent: guarded on a sentinel booking reference. Uses dedicated WH-DEMO-* references so it * never collides with other seeders. To repopulate after items are walked through their lifecycle, * delete the WH-DEMO-* bookings (cascades) and reboot. */ @Injectable() export class WarehouseDemoSeeder { private readonly logger = new Logger(WarehouseDemoSeeder.name); private readonly SENTINEL = 'WH-DEMO-RCV-1'; constructor(private readonly dataSource: DataSource) {} async run(): Promise { const bookingRepo = this.dataSource.getRepository(Booking); if (await bookingRepo.findOne({ where: { reference: this.SENTINEL } })) { this.logger.log('Warehouse demo data already seeded, skipping'); return; } try { const yardRepo = this.dataSource.getRepository(Yard); const serviceTypeRepo = this.dataSource.getRepository(ServiceType); const cargoTypeRepo = this.dataSource.getRepository(CargoType); const warehouseRepo = this.dataSource.getRepository(Warehouse); const whYardRepo = this.dataSource.getRepository(WarehouseYard); const whZoneRepo = this.dataSource.getRepository(WarehouseZone); const invRepo = this.dataSource.getRepository(WarehouseInventory); const djibYard = (await yardRepo.findOne({ where: { code: 'DJIB_PORT' } })) ?? (await yardRepo.findOne({ where: { country: 'Djibouti' } })); const ethYard = (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 } }); if (!djibYard || !ethYard || !serviceType) { this.logger.warn( `Missing yards/service type (djib=${djibYard?.code}, eth=${ethYard?.code}, svc=${serviceType?.code}); skipping`, ); return; } const warehouse = await warehouseRepo.findOne({ where: { code: 'INDODE_OPEN' } }); const whYard = warehouse ? await whYardRepo.findOne({ where: { warehouseId: warehouse.id } }) : null; const whZone = whYard ? await whZoneRepo.findOne({ where: { yardId: whYard.id } }) : null; if (!warehouse || !whYard || !whZone) { this.logger.warn('INDODE_OPEN warehouse/yard/zone missing; skipping warehouse demo seed'); return; } const now = Date.now(); const ago = (mins: number) => new Date(now - mins * 60_000); // EXPORT booking = Ethiopia → Djibouti; IMPORT booking = Djibouti → Ethiopia. const makeBooking = async ( reference: string, direction: 'EXPORT' | 'IMPORT', status: string, weight: number, idx: number, ): Promise => bookingRepo.save( bookingRepo.create({ ...this.demoBookingDefaults(), reference, originYardId: direction === 'EXPORT' ? ethYard.id : djibYard.id, destinationYardId: direction === 'EXPORT' ? djibYard.id : ethYard.id, serviceTypeId: serviceType.id, status, paymentStatus: 'PAID', tradeDirection: direction, freightType: idx % 2 === 0 ? 'CONTAINER' : 'BULK', cargoTypeId: cargoType?.id ?? null, cargoFreeText: cargoType ? null : `${direction} demo cargo ${idx}`, cargoTotalWeightVgm: weight, }), ); const makeInventory = async ( booking: Booking, status: string, weight: number, extra: Partial, ): Promise => { await invRepo.save( invRepo.create({ warehouseId: warehouse.id, yardId: whYard.id, zoneId: whZone.id, bookingId: booking.id, quantity: 1, weight, status: status as WarehouseInventory['status'], notes: '[WH-DEMO]', ...extra, }), ); }; let created = 0; // 1) Export Receive Queue — 3 PAID export bookings, NO inventory. for (let i = 1; i <= 3; i++) { await makeBooking(`WH-DEMO-RCV-${i}`, 'EXPORT', 'PAID', 4000 + i * 500, i); created++; } // 2) Export Ready To Load — EXPORT inventory READY_FOR_LOADING + PASSED. for (let i = 1; i <= 3; i++) { const b = await makeBooking(`WH-DEMO-RTL-${i}`, 'EXPORT', 'PAID', 6000 + i * 500, i); await makeInventory(b, 'READY_FOR_LOADING', 6000 + i * 500, { inspectionStatus: 'PASSED', arrivedAt: ago(180), inspectedAt: ago(120), readyForLoadingAt: ago(60), }); created++; } // 3) Export Loaded / Dispatch Queue — EXPORT inventory LOADED. for (let i = 1; i <= 2; i++) { const b = await makeBooking(`WH-DEMO-LOAD-${i}`, 'EXPORT', 'PAID', 7000 + i * 500, i); await makeInventory(b, 'LOADED', 7000 + i * 500, { inspectionStatus: 'PASSED', arrivedAt: ago(240), inspectedAt: ago(180), readyForLoadingAt: ago(120), loadedAt: ago(30), }); created++; } // 4) Import Unloaded Queue — UNLOADED import inventory (not inspected, not stored). for (let i = 1; i <= 3; i++) { const b = await makeBooking(`WH-DEMO-UNL-${i}`, 'IMPORT', 'IN_TRANSIT', 5000 + i * 500, i); await makeInventory(b, 'UNLOADED', 5000 + i * 500, { arrivedAt: ago(90), unloadedAt: ago(45), }); created++; } // 5) Import Dispatch Queue — READY_FOR_PICKUP import inventory (inspection PASSED). for (let i = 1; i <= 3; i++) { const b = await makeBooking(`WH-DEMO-PKR-${i}`, 'IMPORT', 'IN_TRANSIT', 5500 + i * 500, i); await makeInventory(b, 'READY_FOR_PICKUP', 5500 + i * 500, { inspectionStatus: 'PASSED', arrivedAt: ago(200), unloadedAt: ago(160), inspectedAt: ago(120), readyForPickupAt: ago(60), }); created++; } // 6) Import Arrive Queue — an ARRIVED import train with IN_TRANSIT bookings, no inventory yet. await this.seedArrivedImportTrain(djibYard, ethYard, serviceType, cargoType, ago(60), ago(360)); created += 1; this.logger.log(`✅ Warehouse demo seeded: ${created} buckets populated across every queue`); } catch (error) { this.logger.error( `WarehouseDemoSeeder failed: ${error instanceof Error ? error.message : String(error)}`, ); } } /** An ARRIVED Djibouti→Ethiopia train with 3 IN_TRANSIT bookings (no inventory) for the Arrive Queue. */ private async seedArrivedImportTrain( djibYard: Yard, ethYard: Yard, serviceType: ServiceType, cargoType: CargoType | null, arrival: Date, departure: Date, ): Promise { const bookingRepo = this.dataSource.getRepository(Booking); const locoRepo = this.dataSource.getRepository(Locomotive); const trainSetRepo = this.dataSource.getRepository(TrainSet); const scheduleRepo = this.dataSource.getRepository(TrainSchedule); const scheduleBookingRepo = this.dataSource.getRepository(TrainScheduleBooking); const loco = (await locoRepo.findOne({ where: { code: 'WH-DEMO-LOCO' } })) ?? (await locoRepo.save(locoRepo.create({ code: 'WH-DEMO-LOCO', name: 'Demo Locomotive', maxPullWeightTons: 4000 }))); const trainSet = await trainSetRepo.save( trainSetRepo.create({ locomotiveId: loco.id, totalWeightTons: 500, totalLengthMeters: 300, wagonCount: 10, status: 'COMPLETED', }), ); const schedule = await scheduleRepo.save( scheduleRepo.create({ trainSetId: trainSet.id, originStationId: djibYard.id, destinationStationId: ethYard.id, scheduledDepartureDate: departure, scheduledArrivalDate: arrival, actualArrivalAt: arrival, status: 'ARRIVED' as TrainSchedule['status'], trainNumber: 'WH-DEMO-IMP-TRAIN', }), ); for (let i = 1; i <= 3; i++) { const b = await bookingRepo.save( bookingRepo.create({ ...this.demoBookingDefaults(), reference: `WH-DEMO-ARR-${i}`, originYardId: djibYard.id, destinationYardId: ethYard.id, serviceTypeId: serviceType.id, status: 'IN_TRANSIT', paymentStatus: 'PAID', tradeDirection: 'IMPORT', freightType: i % 2 === 0 ? 'CONTAINER' : 'BULK', cargoTypeId: cargoType?.id ?? null, cargoFreeText: cargoType ? null : `IMPORT arrive demo cargo ${i}`, cargoTotalWeightVgm: 5000 + i * 400, }), ); await scheduleBookingRepo.save( scheduleBookingRepo.create({ trainScheduleId: schedule.id, bookingId: b.id }), ); } } private demoBookingDefaults(): Partial { return { scheduledDate: new Date(), contractType: 'SPOT', equipmentReturn: 'TERMINAL', paymentCurrency: 'ETB', totalAmount: 0, isGovernment: false, }; } }