mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
272 lines
11 KiB
TypeScript
272 lines
11 KiB
TypeScript
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<void> {
|
|
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<Booking> =>
|
|
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<WarehouseInventory>,
|
|
): Promise<void> => {
|
|
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<void> {
|
|
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<Booking> {
|
|
return {
|
|
scheduledDate: new Date(),
|
|
contractType: 'SPOT',
|
|
equipmentReturn: 'TERMINAL',
|
|
paymentCurrency: 'ETB',
|
|
totalAmount: 0,
|
|
isGovernment: false,
|
|
};
|
|
}
|
|
}
|