mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 12:00:59 +00:00
Marshaling document Receive Export and import handover to customer
This commit is contained in:
@@ -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;
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user