Marshaling document Receive Export and import handover to customer

This commit is contained in:
hagiye
2026-06-29 12:24:14 +03:00
parent 2a038c59db
commit d55f9312bf
28 changed files with 2236 additions and 162 deletions

View File

@@ -0,0 +1,85 @@
import 'reflect-metadata';
import { config } from 'dotenv';
import { resolve } from 'path';
import { NestFactory } from '@nestjs/core';
import { DataSource } from 'typeorm';
config({ path: resolve(__dirname, '../../.env') });
process.env.TYPEORM_LOGGING = 'false';
import { AppModule } from '../app.module';
import { deriveTradeDirection } from '../common/derive-trade-direction.util';
import { WarehouseInventoryService } from '../modules/warehouses/warehouse-inventory.service';
async function main() {
const app = await NestFactory.createApplicationContext(AppModule, {
logger: ['error', 'warn'],
});
try {
const dataSource = app.get(DataSource);
const inventory = app.get(WarehouseInventoryService);
const schedules: {
id: string;
trainNumber: string | null;
originCountry: string | null;
destinationCountry: string | null;
}[] = await dataSource.query(
`SELECT ts.id,
ts.train_number AS "trainNumber",
oy.country AS "originCountry",
dy.country AS "destinationCountry"
FROM freight.train_schedules ts
LEFT JOIN freight.yards oy ON oy.id = ts.origin_station_id
LEFT JOIN freight.yards dy ON dy.id = ts.destination_station_id
WHERE ts.status = 'ARRIVED'
AND ts.deleted_at IS NULL
ORDER BY COALESCE(ts.actual_arrival_at, ts.scheduled_arrival_date) DESC NULLS LAST,
ts.created_at DESC`,
);
const importSchedules = schedules.filter(
(schedule) =>
deriveTradeDirection(
{ country: schedule.originCountry },
{ country: schedule.destinationCountry },
) === 'IMPORT',
);
if (importSchedules.length === 0) {
console.log('No ARRIVED import trains found.');
return;
}
for (const schedule of importSchedules) {
const result = await inventory.autoUnloadArrivedBookings(
schedule.id,
'Demo Auto Unload',
);
console.log(
`${schedule.trainNumber ?? schedule.id}: ${result.unloadedCount} unloaded, ${result.skippedCount} skipped, ${result.failedCount} failed`,
);
for (const item of result.results) {
console.log(` - ${item.bookingId}: ${item.status}${item.reason ? ` (${item.reason})` : ''}`);
}
}
const queueRows = await inventory.importUnloadedQueue();
console.log(`Import Unloaded Queue rows now visible: ${queueRows.length}`);
const byStatus = queueRows.reduce<Record<string, number>>((acc, row) => {
acc[row.currentStatus] = (acc[row.currentStatus] ?? 0) + 1;
return acc;
}, {});
for (const [status, count] of Object.entries(byStatus)) {
console.log(` ${status}: ${count}`);
}
} finally {
await app.close();
}
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

View File

@@ -1,16 +1,30 @@
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);
@@ -25,18 +39,34 @@ async function main() {
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',
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' } })) ??
@@ -64,6 +94,78 @@ async function main() {
}),
));
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,
maxWagonsPerTrain: 53,
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,
priorityBonusPoints: 0,
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;
@@ -75,7 +177,7 @@ async function main() {
locomotiveId: locomotive.id,
totalWeightTons: 960,
totalLengthMeters: 420,
wagonCount: 18,
wagonCount: BOOKING_REFS.length,
status: 'COMPLETED',
}),
);
@@ -111,9 +213,169 @@ async function main() {
}
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,
tareWeight: 20,
maxPayloadWeight: 70,
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();