mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 11:21:18 +00:00
- 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.
387 lines
14 KiB
TypeScript
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);
|
|
});
|