Files
edr-platform/apps/edr-freight-api/src/scripts/seed-negad-indode-arrived-train.ts
Marshal ddddcfb71f Refactor wagon type handling and container wagon calculations
- Removed maxWagonsPerTrain from WagonType entity and related DTOs.
- Updated containerWagonsForLines function to calculate required wagons based on container lines more accurately.
- Added unit tests for containerWagonsForLines to ensure correct calculations.
- Adjusted related services and scripts to reflect the removal of maxWagonsPerTrain.
- Enhanced booking and contract components to use new status labels for better user experience.
- Implemented validation for unique container numbers in shipment forms.
2026-07-09 03:36:35 +00:00

387 lines
14 KiB
TypeScript

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);
}
async function main() {
const dataSource = await AppDataSource.initialize();
try {
await dataSource.transaction(async (manager) => {
const yardRepo = manager.getRepository(Yard);
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 / 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' } })) ??
(await yardRepo.save(
yardRepo.create({
code: 'INDODE',
label: 'Indode Dry Port',
country: 'Ethiopia',
isActive: true,
displayOrder: 6,
}),
));
const locomotive =
(await locomotiveRepo.findOne({ where: { code: 'NEGAD-INDODE-LOCO' } })) ??
(await locomotiveRepo.save(
locomotiveRepo.create({
code: 'NEGAD-INDODE-LOCO',
name: 'Negad to Indode Demo Locomotive',
locomotiveType: 'DIESEL',
maxPullWeightTons: 4200,
maxTrainLengthMeters: 760,
status: 'AVAILABLE',
currentYardId: indode.id,
}),
));
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,
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,
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;
let schedule = await scheduleRepo.findOne({ where: { trainNumber: TRAIN_NUMBER } });
if (!schedule) {
const trainSet = await trainSetRepo.save(
trainSetRepo.create({
locomotiveId: locomotive.id,
totalWeightTons: 960,
totalLengthMeters: 420,
wagonCount: BOOKING_REFS.length,
status: 'COMPLETED',
}),
);
schedule = scheduleRepo.create({
trainSetId: trainSet.id,
originStationId: negad.id,
destinationStationId: indode.id,
scheduledDepartureDate: departure,
scheduledArrivalDate: arrival,
actualDepartureAt: departure,
actualArrivalAt: arrival,
status: 'ARRIVED' as TrainSchedule['status'],
trainNumber: TRAIN_NUMBER,
direction: 'IMPORT',
maxWagons: 53,
bookingWindowStatus: 'CLOSED',
});
} else {
schedule.originStationId = negad.id;
schedule.destinationStationId = indode.id;
schedule.scheduledDepartureDate = departure;
schedule.scheduledArrivalDate = arrival;
schedule.actualDepartureAt = departure;
schedule.actualArrivalAt = arrival;
schedule.status = 'ARRIVED' as TrainSchedule['status'];
schedule.direction = 'IMPORT';
schedule.bookingWindowStatus = 'CLOSED';
if (schedule.trainSetId) {
await trainSetRepo.update(schedule.trainSetId, { status: 'COMPLETED' });
}
}
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();
}
}
main().catch((err) => {
console.error('Negad to Indode arrived train seed failed:', err);
process.exit(1);
});