mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
Merge branch 'dev' into freight/feat/invoice
This commit is contained in:
474
apps/edr-freight-api/src/seed/marshalling-demo-trains.seeder.ts
Normal file
474
apps/edr-freight-api/src/seed/marshalling-demo-trains.seeder.ts
Normal file
@@ -0,0 +1,474 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { WagonStatus } from '@edr/types';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { Booking } from '../modules/bookings/entities/booking.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<void> {
|
||||
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;
|
||||
|
||||
const missing = [
|
||||
!djiboutiYard ? 'Djibouti yard' : '',
|
||||
!ethiopiaYard ? 'Ethiopia yard' : '',
|
||||
!serviceType ? 'service type' : '',
|
||||
!wagonType ? 'wagon type' : '',
|
||||
!warehouse ? 'INDODE_OPEN warehouse' : '',
|
||||
!warehouseYard ? 'warehouse yard' : '',
|
||||
!warehouseZone ? 'warehouse zone' : '',
|
||||
].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!,
|
||||
});
|
||||
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;
|
||||
},
|
||||
): Promise<boolean> {
|
||||
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 tareWeight = Number(refs.wagonType.tareWeightTons) || 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,
|
||||
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,
|
||||
tareWeight,
|
||||
capacityTons: wagonCapacity,
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<Locomotive> {
|
||||
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;
|
||||
tareWeight: number;
|
||||
capacityTons: number;
|
||||
dispatched: boolean;
|
||||
}): Promise<Wagon> {
|
||||
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,
|
||||
tareWeight: input.tareWeight,
|
||||
maxPayloadWeight: input.capacityTons,
|
||||
status: WagonStatus.Assigned,
|
||||
notes: 'Marshalling demo seed wagon',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async seedImportOperation(
|
||||
trainScheduleId: string,
|
||||
trainNumber: string,
|
||||
now: Date,
|
||||
departure: Date,
|
||||
dispatched: boolean,
|
||||
): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user