mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 18:20:57 +00:00
resolve conflict
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
||||
import { EdrOrgSeeder } from "./seed/edr-org.seeder";
|
||||
import { DemoUsersSeeder } from "./seed/demo-users.seeder";
|
||||
import { DemoBookingsSeeder } from "./seed/demo-bookings.seeder";
|
||||
import { PricingDataSeeder } from "./seed/pricing-data.seeder";
|
||||
import { FileUploadSettingsSeeder } from "./seed/file-upload-settings.seeder";
|
||||
//New Trains, Wagons, Container and Cargo management modules
|
||||
import { TrainsModule } from "./modules/trains/trains.module";
|
||||
@@ -94,7 +95,7 @@ import { CargoesModule } from './modules/cargoes/cargoes.module';
|
||||
ContainersModule,
|
||||
CargoesModule,
|
||||
],
|
||||
providers: [EdrOrgSeeder, DemoUsersSeeder, DemoBookingsSeeder, FileUploadSettingsSeeder],
|
||||
providers: [EdrOrgSeeder, DemoUsersSeeder, DemoBookingsSeeder, PricingDataSeeder, FileUploadSettingsSeeder],
|
||||
})
|
||||
export class AppModule implements OnApplicationBootstrap {
|
||||
constructor(
|
||||
@@ -102,6 +103,7 @@ export class AppModule implements OnApplicationBootstrap {
|
||||
private readonly edrOrgSeeder: EdrOrgSeeder,
|
||||
private readonly demoUsersSeeder: DemoUsersSeeder,
|
||||
private readonly demoBookingsSeeder: DemoBookingsSeeder,
|
||||
private readonly pricingDataSeeder: PricingDataSeeder,
|
||||
private readonly fileUploadSettingsSeeder: FileUploadSettingsSeeder,
|
||||
) { }
|
||||
|
||||
@@ -110,6 +112,7 @@ export class AppModule implements OnApplicationBootstrap {
|
||||
await this.edrOrgSeeder.run();
|
||||
await this.demoUsersSeeder.run();
|
||||
await this.demoBookingsSeeder.run();
|
||||
await this.pricingDataSeeder.run();
|
||||
await this.fileUploadSettingsSeeder.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +147,18 @@ export class BookingsController {
|
||||
return this.bookingsService.remove(id);
|
||||
}
|
||||
|
||||
@Post(':id/documents')
|
||||
@UseInterceptors(AnyFilesInterceptor())
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiOperation({ summary: 'Upload documents for a booking (DRAFT only)' })
|
||||
async uploadDocuments(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@UploadedFiles() files: Express.Multer.File[],
|
||||
) {
|
||||
const booking = await this.bookingsService.uploadDocuments(id, files ?? []);
|
||||
return this.transitionService.enrichBookingResponse(booking);
|
||||
}
|
||||
|
||||
@Post(':id/generate-price')
|
||||
@ApiOperation({ summary: 'Generate price preview (DRAFT only)' })
|
||||
@ApiOkResponse({ type: GeneratePriceResponseDto })
|
||||
|
||||
@@ -452,6 +452,21 @@ export class BookingsService {
|
||||
return this.findById(booking.id);
|
||||
}
|
||||
|
||||
/** Upload documents for a DRAFT booking. */
|
||||
async uploadDocuments(
|
||||
id: string,
|
||||
files: Express.Multer.File[],
|
||||
): Promise<Booking> {
|
||||
const booking = await this.findById(id);
|
||||
if (booking.status !== 'DRAFT') {
|
||||
throw new BadRequestException(
|
||||
'Documents can only be uploaded for DRAFT bookings',
|
||||
);
|
||||
}
|
||||
await this.filesService.uploadMany(id, 'bookings', files);
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const booking = await this.findById(id);
|
||||
if (booking.status !== 'DRAFT') {
|
||||
|
||||
@@ -3,25 +3,28 @@ import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource, EntityManager, In } from 'typeorm';
|
||||
} from "@nestjs/common";
|
||||
import { InjectDataSource } from "@nestjs/typeorm";
|
||||
import { DataSource, EntityManager, In } from "typeorm";
|
||||
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
import { Locomotive, type LocomotiveStatus } from '../locomotives/entities/locomotive.entity';
|
||||
import { LocomotivesRepository } from '../locomotives/locomotives.repository';
|
||||
import { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity';
|
||||
import { TrainSet } from '../train-sets/entities/train-set.entity';
|
||||
import { TrainScheduleBooking } from '../train-schedules/entities/train-schedule-booking.entity';
|
||||
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
||||
import { WagonBookingAllocation } from '../train-schedules/entities/wagon-booking-allocation.entity';
|
||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||
import { WagonTypesRepository } from '../wagon-types/wagon-types.repository';
|
||||
import { CreateContainerTrainScheduleDto } from './dto/create-container-train-schedule.dto';
|
||||
import { GetEligibleContainerBookingsDto } from './dto/get-eligible-container-bookings.dto';
|
||||
import { PreviewContainerTrainScheduleDto } from './dto/preview-container-train-schedule.dto';
|
||||
import { Booking } from "../bookings/entities/booking.entity";
|
||||
import {
|
||||
Locomotive,
|
||||
type LocomotiveStatus,
|
||||
} from "../locomotives/entities/locomotive.entity";
|
||||
import { LocomotivesRepository } from "../locomotives/locomotives.repository";
|
||||
import { TrainSetWagon } from "../train-sets/entities/train-set-wagon.entity";
|
||||
import { TrainSet } from "../train-sets/entities/train-set.entity";
|
||||
import { TrainScheduleBooking } from "../train-schedules/entities/train-schedule-booking.entity";
|
||||
import { TrainSchedule } from "../train-schedules/entities/train-schedule.entity";
|
||||
import { WagonBookingAllocation } from "../train-schedules/entities/wagon-booking-allocation.entity";
|
||||
import { WagonType } from "../wagon-types/entities/wagon-type.entity";
|
||||
import { WagonTypesRepository } from "../wagon-types/wagon-types.repository";
|
||||
import { CreateContainerTrainScheduleDto } from "./dto/create-container-train-schedule.dto";
|
||||
import { GetEligibleContainerBookingsDto } from "./dto/get-eligible-container-bookings.dto";
|
||||
import { PreviewContainerTrainScheduleDto } from "./dto/preview-container-train-schedule.dto";
|
||||
|
||||
const DEFAULT_WAGON_TYPE_CODE = 'NW5';
|
||||
const DEFAULT_WAGON_TYPE_CODE = "NW5";
|
||||
const MAX_TRAIN_WEIGHT_TONS = 3500;
|
||||
const MAX_TRAIN_LENGTH_METERS = 760;
|
||||
|
||||
@@ -74,11 +77,12 @@ export class TrainSchedulingService {
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly locomotivesRepository: LocomotivesRepository,
|
||||
private readonly wagonTypesRepository: WagonTypesRepository,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
async getEligibleContainerBookings(query: GetEligibleContainerBookingsDto) {
|
||||
const bookingRepository = this.dataSource.getRepository(Booking);
|
||||
const queryBuilder = bookingRepository
|
||||
<<<<<<< HEAD
|
||||
.createQueryBuilder('booking')
|
||||
.leftJoinAndSelect('booking.company', 'company')
|
||||
.leftJoinAndSelect('booking.originYard', 'originYard')
|
||||
@@ -88,17 +92,35 @@ export class TrainSchedulingService {
|
||||
.leftJoin(TrainScheduleBooking, 'scheduleBooking', 'scheduleBooking.booking_id = booking.id')
|
||||
.where('booking.freightType = :freightType', { freightType: 'CONTAINER' })
|
||||
.andWhere('scheduleBooking.id IS NULL');
|
||||
=======
|
||||
.createQueryBuilder("booking")
|
||||
.leftJoinAndSelect("booking.customer", "customer")
|
||||
.leftJoinAndSelect("booking.originYard", "originYard")
|
||||
.leftJoinAndSelect("booking.destinationYard", "destinationYard")
|
||||
.leftJoinAndSelect("booking.bookingContainers", "bookingContainer")
|
||||
.leftJoinAndSelect("bookingContainer.containerType", "containerType")
|
||||
.leftJoin(
|
||||
TrainScheduleBooking,
|
||||
"scheduleBooking",
|
||||
"scheduleBooking.booking_id = booking.id",
|
||||
)
|
||||
.where("booking.freightType = :freightType", { freightType: "CONTAINER" })
|
||||
.andWhere("scheduleBooking.id IS NULL");
|
||||
>>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8
|
||||
|
||||
if (query.originStationId) {
|
||||
queryBuilder.andWhere('booking.originYardId = :originStationId', {
|
||||
queryBuilder.andWhere("booking.originYardId = :originStationId", {
|
||||
originStationId: query.originStationId,
|
||||
});
|
||||
}
|
||||
|
||||
if (query.destinationStationId) {
|
||||
queryBuilder.andWhere('booking.destinationYardId = :destinationStationId', {
|
||||
destinationStationId: query.destinationStationId,
|
||||
});
|
||||
queryBuilder.andWhere(
|
||||
"booking.destinationYardId = :destinationStationId",
|
||||
{
|
||||
destinationStationId: query.destinationStationId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (query.scheduleDate) {
|
||||
@@ -109,25 +131,52 @@ export class TrainSchedulingService {
|
||||
}
|
||||
|
||||
if (query.status) {
|
||||
queryBuilder.andWhere('booking.status = :status', { status: query.status });
|
||||
queryBuilder.andWhere("booking.status = :status", {
|
||||
status: query.status,
|
||||
});
|
||||
}
|
||||
|
||||
const bookings = await queryBuilder
|
||||
.orderBy('booking.scheduled_date', 'ASC')
|
||||
.addOrderBy('booking.created_at', 'ASC')
|
||||
.orderBy("booking.scheduled_date", "ASC")
|
||||
.addOrderBy("booking.created_at", "ASC")
|
||||
.getMany();
|
||||
|
||||
const items: EligibleBookingItem[] = bookings.map((booking) => ({
|
||||
id: booking.id,
|
||||
reference: booking.reference,
|
||||
<<<<<<< HEAD
|
||||
customer: booking.company?.name ?? booking.company?.email ?? 'Unknown customer',
|
||||
containerType: booking.bookingContainers
|
||||
?.map((container) => container.containerType?.label ?? container.containerType?.code ?? 'Container')
|
||||
.join(', ') ?? 'Container',
|
||||
quantity: booking.bookingContainers?.reduce((sum, container) => sum + Number(container.quantity ?? 0), 0) ?? 0,
|
||||
=======
|
||||
customer:
|
||||
booking.company?.name ?? booking.company?.email ?? "Unknown customer",
|
||||
containerType:
|
||||
booking.bookingContainers
|
||||
?.map(
|
||||
(container) =>
|
||||
container.containerType?.label ??
|
||||
container.containerType?.code ??
|
||||
"Container",
|
||||
)
|
||||
.join(", ") ?? "Container",
|
||||
quantity:
|
||||
booking.bookingContainers?.reduce(
|
||||
(sum, container) => sum + Number(container.quantity ?? 0),
|
||||
0,
|
||||
) ?? 0,
|
||||
>>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8
|
||||
weightTons: this.roundTons(booking.cargoTotalWeightVgm),
|
||||
origin: booking.originYard?.label ?? booking.originYard?.code ?? 'Unknown origin',
|
||||
destination: booking.destinationYard?.label ?? booking.destinationYard?.code ?? 'Unknown destination',
|
||||
origin:
|
||||
booking.originYard?.label ??
|
||||
booking.originYard?.code ??
|
||||
"Unknown origin",
|
||||
destination:
|
||||
booking.destinationYard?.label ??
|
||||
booking.destinationYard?.code ??
|
||||
"Unknown destination",
|
||||
preferredDepartureDate: booking.scheduledDate.toISOString(),
|
||||
status: booking.status,
|
||||
}));
|
||||
@@ -155,7 +204,7 @@ export class TrainSchedulingService {
|
||||
|
||||
if (!validation.valid) {
|
||||
throw new BadRequestException({
|
||||
message: 'train_schedule_invalid',
|
||||
message: "train_schedule_invalid",
|
||||
violations: validation.violations,
|
||||
});
|
||||
}
|
||||
@@ -165,92 +214,115 @@ export class TrainSchedulingService {
|
||||
validation.summary.totalWeightTons,
|
||||
);
|
||||
|
||||
const createdSchedule = await this.dataSource.transaction(async (manager) => {
|
||||
const locomotiveRepository = manager.getRepository(Locomotive);
|
||||
const lockedLocomotive = await locomotiveRepository.findOne({
|
||||
where: { id: locomotive.id },
|
||||
lock: { mode: 'pessimistic_write' },
|
||||
});
|
||||
const createdSchedule = await this.dataSource.transaction(
|
||||
async (manager) => {
|
||||
const locomotiveRepository = manager.getRepository(Locomotive);
|
||||
const lockedLocomotive = await locomotiveRepository.findOne({
|
||||
where: { id: locomotive.id },
|
||||
lock: { mode: "pessimistic_write" },
|
||||
});
|
||||
|
||||
if (!lockedLocomotive) {
|
||||
throw new NotFoundException(`Locomotive ${locomotive.id} not found`);
|
||||
}
|
||||
|
||||
if (lockedLocomotive.status !== 'AVAILABLE') {
|
||||
throw new ConflictException(`Locomotive ${lockedLocomotive.code} is not available`);
|
||||
}
|
||||
|
||||
if (Number(lockedLocomotive.maxPullWeightTons) < validation.summary.totalWeightTons) {
|
||||
throw new BadRequestException(
|
||||
`Locomotive ${lockedLocomotive.code} cannot pull ${validation.summary.totalWeightTons}T`,
|
||||
);
|
||||
}
|
||||
|
||||
const existingScheduleCount = await manager.getRepository(TrainScheduleBooking).count({
|
||||
where: { bookingId: In(validation.bookings.map((booking) => booking.id)) },
|
||||
});
|
||||
|
||||
if (existingScheduleCount > 0) {
|
||||
throw new BadRequestException('One or more bookings are already scheduled');
|
||||
}
|
||||
|
||||
const trainSet = await this.buildTrainSet(
|
||||
manager,
|
||||
lockedLocomotive,
|
||||
validation.wagonType,
|
||||
validation.summary.totalWeightTons,
|
||||
validation.summary.totalLengthMeters,
|
||||
validation.wagonPlan,
|
||||
);
|
||||
|
||||
const schedule = manager.getRepository(TrainSchedule).create({
|
||||
trainSetId: trainSet.id,
|
||||
originStationId: dto.originStationId,
|
||||
destinationStationId: dto.destinationStationId,
|
||||
scheduledDepartureDate: new Date(dto.scheduleDate),
|
||||
status: 'SCHEDULED',
|
||||
});
|
||||
|
||||
const savedSchedule = await manager.getRepository(TrainSchedule).save(schedule);
|
||||
|
||||
const scheduleBookings = validation.bookings.map((booking) =>
|
||||
manager.getRepository(TrainScheduleBooking).create({
|
||||
trainScheduleId: savedSchedule.id,
|
||||
bookingId: booking.id,
|
||||
}),
|
||||
);
|
||||
await manager.getRepository(TrainScheduleBooking).save(scheduleBookings);
|
||||
|
||||
const savedWagons = await manager.getRepository(TrainSetWagon).find({
|
||||
where: { trainSetId: trainSet.id },
|
||||
order: { sequenceNo: 'ASC' },
|
||||
});
|
||||
|
||||
const wagonBySequence = new Map(savedWagons.map((wagon) => [wagon.sequenceNo, wagon]));
|
||||
const allocationRows = validation.wagonPlan.flatMap((wagonPlan) => {
|
||||
const wagon = wagonBySequence.get(wagonPlan.sequenceNo);
|
||||
|
||||
if (!wagon) {
|
||||
throw new BadRequestException(`Missing wagon sequence ${wagonPlan.sequenceNo}`);
|
||||
if (!lockedLocomotive) {
|
||||
throw new NotFoundException(`Locomotive ${locomotive.id} not found`);
|
||||
}
|
||||
|
||||
return wagonPlan.allocations.map((allocation) =>
|
||||
manager.getRepository(WagonBookingAllocation).create({
|
||||
trainSetWagonId: wagon.id,
|
||||
bookingId: allocation.bookingId,
|
||||
allocatedWeightTons: allocation.allocatedWeightTons,
|
||||
if (lockedLocomotive.status !== "AVAILABLE") {
|
||||
throw new ConflictException(
|
||||
`Locomotive ${lockedLocomotive.code} is not available`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
Number(lockedLocomotive.maxPullWeightTons) <
|
||||
validation.summary.totalWeightTons
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Locomotive ${lockedLocomotive.code} cannot pull ${validation.summary.totalWeightTons}T`,
|
||||
);
|
||||
}
|
||||
|
||||
const existingScheduleCount = await manager
|
||||
.getRepository(TrainScheduleBooking)
|
||||
.count({
|
||||
where: {
|
||||
bookingId: In(validation.bookings.map((booking) => booking.id)),
|
||||
},
|
||||
});
|
||||
|
||||
if (existingScheduleCount > 0) {
|
||||
throw new BadRequestException(
|
||||
"One or more bookings are already scheduled",
|
||||
);
|
||||
}
|
||||
|
||||
const trainSet = await this.buildTrainSet(
|
||||
manager,
|
||||
lockedLocomotive,
|
||||
validation.wagonType,
|
||||
validation.summary.totalWeightTons,
|
||||
validation.summary.totalLengthMeters,
|
||||
validation.wagonPlan,
|
||||
);
|
||||
|
||||
const schedule = manager.getRepository(TrainSchedule).create({
|
||||
trainSetId: trainSet.id,
|
||||
originStationId: dto.originStationId,
|
||||
destinationStationId: dto.destinationStationId,
|
||||
scheduledDepartureDate: new Date(dto.scheduleDate),
|
||||
status: "SCHEDULED",
|
||||
});
|
||||
|
||||
const savedSchedule = await manager
|
||||
.getRepository(TrainSchedule)
|
||||
.save(schedule);
|
||||
|
||||
const scheduleBookings = validation.bookings.map((booking) =>
|
||||
manager.getRepository(TrainScheduleBooking).create({
|
||||
trainScheduleId: savedSchedule.id,
|
||||
bookingId: booking.id,
|
||||
}),
|
||||
);
|
||||
});
|
||||
await manager
|
||||
.getRepository(TrainScheduleBooking)
|
||||
.save(scheduleBookings);
|
||||
|
||||
await manager.getRepository(WagonBookingAllocation).save(allocationRows);
|
||||
const savedWagons = await manager.getRepository(TrainSetWagon).find({
|
||||
where: { trainSetId: trainSet.id },
|
||||
order: { sequenceNo: "ASC" },
|
||||
});
|
||||
|
||||
await locomotiveRepository.update(lockedLocomotive.id, {
|
||||
status: 'ASSIGNED',
|
||||
});
|
||||
const wagonBySequence = new Map(
|
||||
savedWagons.map((wagon) => [wagon.sequenceNo, wagon]),
|
||||
);
|
||||
const allocationRows = validation.wagonPlan.flatMap((wagonPlan) => {
|
||||
const wagon = wagonBySequence.get(wagonPlan.sequenceNo);
|
||||
|
||||
return savedSchedule.id;
|
||||
});
|
||||
if (!wagon) {
|
||||
throw new BadRequestException(
|
||||
`Missing wagon sequence ${wagonPlan.sequenceNo}`,
|
||||
);
|
||||
}
|
||||
|
||||
return wagonPlan.allocations.map((allocation) =>
|
||||
manager.getRepository(WagonBookingAllocation).create({
|
||||
trainSetWagonId: wagon.id,
|
||||
bookingId: allocation.bookingId,
|
||||
allocatedWeightTons: allocation.allocatedWeightTons,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await manager
|
||||
.getRepository(WagonBookingAllocation)
|
||||
.save(allocationRows);
|
||||
|
||||
await locomotiveRepository.update(lockedLocomotive.id, {
|
||||
status: "ASSIGNED",
|
||||
});
|
||||
|
||||
return savedSchedule.id;
|
||||
},
|
||||
);
|
||||
|
||||
return this.getContainerTrainScheduleById(createdSchedule);
|
||||
}
|
||||
@@ -261,7 +333,7 @@ export class TrainSchedulingService {
|
||||
const bookingIds = [...new Set(dto.bookingIds)];
|
||||
|
||||
if (!bookingIds.length) {
|
||||
throw new BadRequestException('At least one booking is required');
|
||||
throw new BadRequestException("At least one booking is required");
|
||||
}
|
||||
|
||||
const [wagonType] = await this.wagonTypesRepository.findAll({
|
||||
@@ -269,7 +341,9 @@ export class TrainSchedulingService {
|
||||
});
|
||||
|
||||
if (!wagonType) {
|
||||
throw new NotFoundException(`Wagon type ${DEFAULT_WAGON_TYPE_CODE} not found`);
|
||||
throw new NotFoundException(
|
||||
`Wagon type ${DEFAULT_WAGON_TYPE_CODE} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
const bookings = await this.loadBookingsForScheduling(bookingIds);
|
||||
@@ -278,21 +352,29 @@ export class TrainSchedulingService {
|
||||
if (bookings.length !== bookingIds.length) {
|
||||
const foundIds = new Set(bookings.map((booking) => booking.id));
|
||||
const missing = bookingIds.filter((id) => !foundIds.has(id));
|
||||
violations.push(`Bookings not found: ${missing.join(', ')}`);
|
||||
violations.push(`Bookings not found: ${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
const scheduledLinks = await this.dataSource.getRepository(TrainScheduleBooking).find({
|
||||
where: { bookingId: In(bookingIds) },
|
||||
select: { bookingId: true },
|
||||
});
|
||||
const scheduledLinks = await this.dataSource
|
||||
.getRepository(TrainScheduleBooking)
|
||||
.find({
|
||||
where: { bookingId: In(bookingIds) },
|
||||
select: { bookingId: true },
|
||||
});
|
||||
|
||||
if (scheduledLinks.length > 0) {
|
||||
violations.push('One or more selected bookings are already assigned to a train schedule');
|
||||
violations.push(
|
||||
"One or more selected bookings are already assigned to a train schedule",
|
||||
);
|
||||
}
|
||||
|
||||
const nonContainerBookings = bookings.filter((booking) => booking.freightType !== 'CONTAINER');
|
||||
const nonContainerBookings = bookings.filter(
|
||||
(booking) => booking.freightType !== "CONTAINER",
|
||||
);
|
||||
if (nonContainerBookings.length > 0) {
|
||||
violations.push('Only CONTAINER bookings are supported for train scheduling');
|
||||
violations.push(
|
||||
"Only CONTAINER bookings are supported for train scheduling",
|
||||
);
|
||||
}
|
||||
|
||||
const scheduleDateKey = this.toUtcDateKey(dto.scheduleDate);
|
||||
@@ -302,44 +384,62 @@ export class TrainSchedulingService {
|
||||
booking.destinationYardId !== dto.destinationStationId,
|
||||
);
|
||||
if (routeMismatch) {
|
||||
violations.push('Selected bookings must share the same origin and destination as the schedule');
|
||||
violations.push(
|
||||
"Selected bookings must share the same origin and destination as the schedule",
|
||||
);
|
||||
}
|
||||
|
||||
const dateMismatch = bookings.some(
|
||||
(booking) => this.toUtcDateKey(booking.scheduledDate) !== scheduleDateKey,
|
||||
);
|
||||
if (dateMismatch) {
|
||||
violations.push('Selected bookings must share the same schedule date');
|
||||
violations.push("Selected bookings must share the same schedule date");
|
||||
}
|
||||
|
||||
const uniqueOriginCount = new Set(bookings.map((booking) => booking.originYardId)).size;
|
||||
const uniqueOriginCount = new Set(
|
||||
bookings.map((booking) => booking.originYardId),
|
||||
).size;
|
||||
if (uniqueOriginCount > 1) {
|
||||
violations.push('Selected bookings must share the same origin station');
|
||||
violations.push("Selected bookings must share the same origin station");
|
||||
}
|
||||
|
||||
const uniqueDestinationCount = new Set(bookings.map((booking) => booking.destinationYardId)).size;
|
||||
const uniqueDestinationCount = new Set(
|
||||
bookings.map((booking) => booking.destinationYardId),
|
||||
).size;
|
||||
if (uniqueDestinationCount > 1) {
|
||||
violations.push('Selected bookings must share the same destination station');
|
||||
violations.push(
|
||||
"Selected bookings must share the same destination station",
|
||||
);
|
||||
}
|
||||
|
||||
const uniqueDateCount = new Set(
|
||||
bookings.map((booking) => this.toUtcDateKey(booking.scheduledDate)),
|
||||
).size;
|
||||
if (uniqueDateCount > 1) {
|
||||
violations.push('Selected bookings must share the same preferred departure date');
|
||||
violations.push(
|
||||
"Selected bookings must share the same preferred departure date",
|
||||
);
|
||||
}
|
||||
|
||||
const totalWeightTons = this.roundTons(
|
||||
bookings.reduce((sum, booking) => sum + Number(booking.cargoTotalWeightVgm ?? 0), 0),
|
||||
bookings.reduce(
|
||||
(sum, booking) => sum + Number(booking.cargoTotalWeightVgm ?? 0),
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
const wagonPlan = this.allocateBookingsToWagons(bookings, this.calculateNW5WagonPlan(totalWeightTons, wagonType));
|
||||
const wagonPlan = this.allocateBookingsToWagons(
|
||||
bookings,
|
||||
this.calculateNW5WagonPlan(totalWeightTons, wagonType),
|
||||
);
|
||||
const totalLengthMeters = this.roundTons(
|
||||
wagonPlan.reduce((sum, wagon) => sum + wagon.lengthMeters, 0),
|
||||
);
|
||||
|
||||
if (totalWeightTons > MAX_TRAIN_WEIGHT_TONS) {
|
||||
violations.push(`Total booking weight ${totalWeightTons}T exceeds max train weight ${MAX_TRAIN_WEIGHT_TONS}T`);
|
||||
violations.push(
|
||||
`Total booking weight ${totalWeightTons}T exceeds max train weight ${MAX_TRAIN_WEIGHT_TONS}T`,
|
||||
);
|
||||
}
|
||||
|
||||
if (totalLengthMeters > MAX_TRAIN_LENGTH_METERS) {
|
||||
@@ -357,21 +457,25 @@ export class TrainSchedulingService {
|
||||
);
|
||||
}
|
||||
|
||||
const availableLocomotiveCount = await this.dataSource.getRepository(Locomotive).count({
|
||||
where: { status: 'AVAILABLE' as LocomotiveStatus },
|
||||
});
|
||||
const availableLocomotiveCount = await this.dataSource
|
||||
.getRepository(Locomotive)
|
||||
.count({
|
||||
where: { status: "AVAILABLE" as LocomotiveStatus },
|
||||
});
|
||||
|
||||
if (availableLocomotiveCount === 0) {
|
||||
violations.push('No available locomotive exists for scheduling');
|
||||
violations.push("No available locomotive exists for scheduling");
|
||||
} else {
|
||||
const capableLocomotives = await this.dataSource.getRepository(Locomotive).find({
|
||||
where: { status: 'AVAILABLE' },
|
||||
});
|
||||
const capableLocomotives = await this.dataSource
|
||||
.getRepository(Locomotive)
|
||||
.find({
|
||||
where: { status: "AVAILABLE" },
|
||||
});
|
||||
const canPull = capableLocomotives.some(
|
||||
(locomotive) => Number(locomotive.maxPullWeightTons) >= totalWeightTons,
|
||||
);
|
||||
if (!canPull) {
|
||||
violations.push('No available locomotive can pull the total weight');
|
||||
violations.push("No available locomotive can pull the total weight");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,14 +495,21 @@ export class TrainSchedulingService {
|
||||
};
|
||||
}
|
||||
|
||||
calculateNW5WagonPlan(totalBookingWeightTons: number, wagonType: WagonType): WagonPlanRecord[] {
|
||||
calculateNW5WagonPlan(
|
||||
totalBookingWeightTons: number,
|
||||
wagonType: WagonType,
|
||||
): WagonPlanRecord[] {
|
||||
const wagonCapacityTons = Number(wagonType.capacityTons);
|
||||
const wagonsNeeded = Math.ceil(totalBookingWeightTons / wagonCapacityTons);
|
||||
let remainingWeight = this.roundTons(totalBookingWeightTons);
|
||||
|
||||
return Array.from({ length: wagonsNeeded }, (_, index) => {
|
||||
const assignedWeightTons = this.roundTons(Math.min(wagonCapacityTons, remainingWeight));
|
||||
remainingWeight = this.roundTons(Math.max(0, remainingWeight - assignedWeightTons));
|
||||
const assignedWeightTons = this.roundTons(
|
||||
Math.min(wagonCapacityTons, remainingWeight),
|
||||
);
|
||||
remainingWeight = this.roundTons(
|
||||
Math.max(0, remainingWeight - assignedWeightTons),
|
||||
);
|
||||
|
||||
return {
|
||||
sequenceNo: index + 1,
|
||||
@@ -410,15 +521,20 @@ export class TrainSchedulingService {
|
||||
});
|
||||
}
|
||||
|
||||
async selectOrValidateLocomotive(locomotiveId: string, totalWeightTons: number) {
|
||||
async selectOrValidateLocomotive(
|
||||
locomotiveId: string,
|
||||
totalWeightTons: number,
|
||||
) {
|
||||
const locomotive = await this.locomotivesRepository.findById(locomotiveId);
|
||||
|
||||
if (!locomotive) {
|
||||
throw new NotFoundException(`Locomotive ${locomotiveId} not found`);
|
||||
}
|
||||
|
||||
if (locomotive.status !== 'AVAILABLE') {
|
||||
throw new BadRequestException(`Locomotive ${locomotive.code} is not available`);
|
||||
if (locomotive.status !== "AVAILABLE") {
|
||||
throw new BadRequestException(
|
||||
`Locomotive ${locomotive.code} is not available`,
|
||||
);
|
||||
}
|
||||
|
||||
if (Number(locomotive.maxPullWeightTons) < totalWeightTons) {
|
||||
@@ -443,7 +559,7 @@ export class TrainSchedulingService {
|
||||
totalWeightTons,
|
||||
totalLengthMeters,
|
||||
wagonCount: wagonPlan.length,
|
||||
status: 'ASSIGNED',
|
||||
status: "ASSIGNED",
|
||||
});
|
||||
const savedTrainSet = await manager.getRepository(TrainSet).save(trainSet);
|
||||
|
||||
@@ -463,11 +579,16 @@ export class TrainSchedulingService {
|
||||
return savedTrainSet;
|
||||
}
|
||||
|
||||
allocateBookingsToWagons(bookings: Booking[], baseWagonPlan: WagonPlanRecord[]): WagonPlanRecord[] {
|
||||
allocateBookingsToWagons(
|
||||
bookings: Booking[],
|
||||
baseWagonPlan: WagonPlanRecord[],
|
||||
): WagonPlanRecord[] {
|
||||
const remaining = bookings.map((booking) => ({
|
||||
bookingId: booking.id,
|
||||
bookingReference: booking.reference,
|
||||
remainingWeightTons: this.roundTons(Number(booking.cargoTotalWeightVgm ?? 0)),
|
||||
remainingWeightTons: this.roundTons(
|
||||
Number(booking.cargoTotalWeightVgm ?? 0),
|
||||
),
|
||||
}));
|
||||
let bookingIndex = 0;
|
||||
|
||||
@@ -496,7 +617,9 @@ export class TrainSchedulingService {
|
||||
booking.remainingWeightTons - allocatedWeightTons,
|
||||
);
|
||||
wagonRemaining = this.roundTons(wagonRemaining - allocatedWeightTons);
|
||||
assignedWeightTons = this.roundTons(assignedWeightTons + allocatedWeightTons);
|
||||
assignedWeightTons = this.roundTons(
|
||||
assignedWeightTons + allocatedWeightTons,
|
||||
);
|
||||
|
||||
if (booking.remainingWeightTons <= 0) {
|
||||
bookingIndex += 1;
|
||||
@@ -519,31 +642,39 @@ export class TrainSchedulingService {
|
||||
destinationStation: true,
|
||||
scheduleBookings: true,
|
||||
},
|
||||
order: { scheduledDepartureDate: 'DESC', createdAt: 'DESC' },
|
||||
order: { scheduledDepartureDate: "DESC", createdAt: "DESC" },
|
||||
});
|
||||
|
||||
return schedules.map((schedule) => ({
|
||||
id: schedule.id,
|
||||
scheduleDate: schedule.scheduledDepartureDate,
|
||||
origin: schedule.originStation?.label ?? schedule.originStation?.code ?? null,
|
||||
origin:
|
||||
schedule.originStation?.label ?? schedule.originStation?.code ?? null,
|
||||
destination:
|
||||
schedule.destinationStation?.label ?? schedule.destinationStation?.code ?? null,
|
||||
schedule.destinationStation?.label ??
|
||||
schedule.destinationStation?.code ??
|
||||
null,
|
||||
locomotive: schedule.trainSet?.locomotive
|
||||
? {
|
||||
id: schedule.trainSet.locomotive.id,
|
||||
code: schedule.trainSet.locomotive.code,
|
||||
name: schedule.trainSet.locomotive.name ?? null,
|
||||
}
|
||||
id: schedule.trainSet.locomotive.id,
|
||||
code: schedule.trainSet.locomotive.code,
|
||||
name: schedule.trainSet.locomotive.name ?? null,
|
||||
}
|
||||
: null,
|
||||
wagonCount: schedule.trainSet?.wagonCount ?? 0,
|
||||
totalWeightTons: this.roundTons(Number(schedule.trainSet?.totalWeightTons ?? 0)),
|
||||
totalLengthMeters: this.roundTons(Number(schedule.trainSet?.totalLengthMeters ?? 0)),
|
||||
totalWeightTons: this.roundTons(
|
||||
Number(schedule.trainSet?.totalWeightTons ?? 0),
|
||||
),
|
||||
totalLengthMeters: this.roundTons(
|
||||
Number(schedule.trainSet?.totalLengthMeters ?? 0),
|
||||
),
|
||||
bookingsCount: schedule.scheduleBookings?.length ?? 0,
|
||||
status: schedule.status,
|
||||
}));
|
||||
}
|
||||
|
||||
async getContainerTrainScheduleById(id: string) {
|
||||
<<<<<<< HEAD
|
||||
const schedule = await this.dataSource.getRepository(TrainSchedule).findOne({
|
||||
where: { id },
|
||||
relations: {
|
||||
@@ -553,6 +684,24 @@ export class TrainSchedulingService {
|
||||
scheduleBookings: { booking: { company: true, originYard: true, destinationYard: true } },
|
||||
},
|
||||
});
|
||||
=======
|
||||
const schedule = await this.dataSource
|
||||
.getRepository(TrainSchedule)
|
||||
.findOne({
|
||||
where: { id },
|
||||
relations: {
|
||||
trainSet: {
|
||||
locomotive: true,
|
||||
wagons: { wagonType: true, allocations: { booking: true } },
|
||||
},
|
||||
originStation: true,
|
||||
destinationStation: true,
|
||||
scheduleBookings: {
|
||||
booking: { company: true, originYard: true, destinationYard: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
>>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8
|
||||
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${id} not found`);
|
||||
@@ -567,47 +716,54 @@ export class TrainSchedulingService {
|
||||
destinationStation: schedule.destinationStation,
|
||||
trainSet: schedule.trainSet
|
||||
? {
|
||||
id: schedule.trainSet.id,
|
||||
status: schedule.trainSet.status,
|
||||
wagonCount: schedule.trainSet.wagonCount,
|
||||
totalWeightTons: this.roundTons(Number(schedule.trainSet.totalWeightTons)),
|
||||
totalLengthMeters: this.roundTons(Number(schedule.trainSet.totalLengthMeters)),
|
||||
locomotive: schedule.trainSet.locomotive
|
||||
? {
|
||||
id: schedule.trainSet.locomotive.id,
|
||||
code: schedule.trainSet.locomotive.code,
|
||||
name: schedule.trainSet.locomotive.name,
|
||||
status: schedule.trainSet.locomotive.status,
|
||||
maxPullWeightTons: this.roundTons(
|
||||
Number(schedule.trainSet.locomotive.maxPullWeightTons),
|
||||
),
|
||||
id: schedule.trainSet.id,
|
||||
status: schedule.trainSet.status,
|
||||
wagonCount: schedule.trainSet.wagonCount,
|
||||
totalWeightTons: this.roundTons(
|
||||
Number(schedule.trainSet.totalWeightTons),
|
||||
),
|
||||
totalLengthMeters: this.roundTons(
|
||||
Number(schedule.trainSet.totalLengthMeters),
|
||||
),
|
||||
locomotive: schedule.trainSet.locomotive
|
||||
? {
|
||||
id: schedule.trainSet.locomotive.id,
|
||||
code: schedule.trainSet.locomotive.code,
|
||||
name: schedule.trainSet.locomotive.name,
|
||||
status: schedule.trainSet.locomotive.status,
|
||||
maxPullWeightTons: this.roundTons(
|
||||
Number(schedule.trainSet.locomotive.maxPullWeightTons),
|
||||
),
|
||||
}
|
||||
: null,
|
||||
wagons: [...(schedule.trainSet.wagons ?? [])]
|
||||
.sort((left, right) => left.sequenceNo - right.sequenceNo)
|
||||
.map((wagon) => ({
|
||||
id: wagon.id,
|
||||
sequenceNo: wagon.sequenceNo,
|
||||
capacityTons: this.roundTons(Number(wagon.capacityTons)),
|
||||
lengthMeters: this.roundTons(Number(wagon.lengthMeters)),
|
||||
assignedWeightTons: this.roundTons(
|
||||
Number(wagon.assignedWeightTons),
|
||||
),
|
||||
wagonType: wagon.wagonType
|
||||
? {
|
||||
id: wagon.wagonType.id,
|
||||
code: wagon.wagonType.code,
|
||||
name: wagon.wagonType.name,
|
||||
}
|
||||
: null,
|
||||
wagons:
|
||||
[...(schedule.trainSet.wagons ?? [])]
|
||||
.sort((left, right) => left.sequenceNo - right.sequenceNo)
|
||||
.map((wagon) => ({
|
||||
id: wagon.id,
|
||||
sequenceNo: wagon.sequenceNo,
|
||||
capacityTons: this.roundTons(Number(wagon.capacityTons)),
|
||||
lengthMeters: this.roundTons(Number(wagon.lengthMeters)),
|
||||
assignedWeightTons: this.roundTons(Number(wagon.assignedWeightTons)),
|
||||
wagonType: wagon.wagonType
|
||||
? {
|
||||
id: wagon.wagonType.id,
|
||||
code: wagon.wagonType.code,
|
||||
name: wagon.wagonType.name,
|
||||
}
|
||||
: null,
|
||||
allocations:
|
||||
wagon.allocations?.map((allocation) => ({
|
||||
id: allocation.id,
|
||||
bookingId: allocation.bookingId,
|
||||
bookingReference: allocation.booking?.reference ?? null,
|
||||
allocatedWeightTons: this.roundTons(Number(allocation.allocatedWeightTons)),
|
||||
})) ?? [],
|
||||
})),
|
||||
}
|
||||
: null,
|
||||
allocations:
|
||||
wagon.allocations?.map((allocation) => ({
|
||||
id: allocation.id,
|
||||
bookingId: allocation.bookingId,
|
||||
bookingReference: allocation.booking?.reference ?? null,
|
||||
allocatedWeightTons: this.roundTons(
|
||||
Number(allocation.allocatedWeightTons),
|
||||
),
|
||||
})) ?? [],
|
||||
})),
|
||||
}
|
||||
: null,
|
||||
bookings:
|
||||
schedule.scheduleBookings?.map((scheduleBooking) => ({
|
||||
@@ -617,17 +773,21 @@ export class TrainSchedulingService {
|
||||
scheduleBooking.booking?.company?.name ??
|
||||
scheduleBooking.booking?.company?.email ??
|
||||
null,
|
||||
weightTons: this.roundTons(Number(scheduleBooking.booking?.cargoTotalWeightVgm ?? 0)),
|
||||
weightTons: this.roundTons(
|
||||
Number(scheduleBooking.booking?.cargoTotalWeightVgm ?? 0),
|
||||
),
|
||||
status: scheduleBooking.booking?.status ?? null,
|
||||
})) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async cancelTrainSchedule(id: string) {
|
||||
const schedule = await this.dataSource.getRepository(TrainSchedule).findOne({
|
||||
where: { id },
|
||||
relations: { trainSet: { locomotive: true } },
|
||||
});
|
||||
const schedule = await this.dataSource
|
||||
.getRepository(TrainSchedule)
|
||||
.findOne({
|
||||
where: { id },
|
||||
relations: { trainSet: { locomotive: true } },
|
||||
});
|
||||
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${id} not found`);
|
||||
@@ -635,19 +795,21 @@ export class TrainSchedulingService {
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await manager.getRepository(TrainSchedule).update(schedule.id, {
|
||||
status: 'CANCELLED',
|
||||
status: "CANCELLED",
|
||||
});
|
||||
|
||||
if (schedule.trainSetId) {
|
||||
await manager.getRepository(TrainSet).update(schedule.trainSetId, {
|
||||
status: 'CANCELLED',
|
||||
status: "CANCELLED",
|
||||
});
|
||||
}
|
||||
|
||||
if (schedule.trainSet?.locomotiveId) {
|
||||
await manager.getRepository(Locomotive).update(schedule.trainSet.locomotiveId, {
|
||||
status: 'AVAILABLE',
|
||||
});
|
||||
await manager
|
||||
.getRepository(Locomotive)
|
||||
.update(schedule.trainSet.locomotiveId, {
|
||||
status: "AVAILABLE",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -663,7 +825,7 @@ export class TrainSchedulingService {
|
||||
destinationYard: true,
|
||||
bookingContainers: { containerType: true },
|
||||
},
|
||||
order: { createdAt: 'ASC' },
|
||||
order: { createdAt: "ASC" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -673,7 +835,7 @@ export class TrainSchedulingService {
|
||||
}
|
||||
|
||||
private roundTons(value: number | string | null | undefined) {
|
||||
const numericValue = typeof value === 'number' ? value : Number(value ?? 0);
|
||||
const numericValue = typeof value === "number" ? value : Number(value ?? 0);
|
||||
|
||||
if (!Number.isFinite(numericValue)) {
|
||||
return 0;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { randomUUID } from "crypto";
|
||||
import { DataSource } from "typeorm";
|
||||
|
||||
<<<<<<< HEAD
|
||||
import { BookingContainer } from '../modules/bookings/entities/booking-container.entity';
|
||||
import { Booking } from '../modules/bookings/entities/booking.entity';
|
||||
import { Company } from '../modules/companies/entities/company.entity';
|
||||
@@ -10,86 +11,106 @@ import { ServiceType } from '../modules/rule-engine/entities/service-type.entity
|
||||
import { Yard } from '../modules/rule-engine/entities/yard.entity';
|
||||
import { WagonType } from '../modules/wagon-types/entities/wagon-type.entity';
|
||||
import { ContainerType } from '../modules/rule-engine/entities/container-type.entity';
|
||||
=======
|
||||
import { BookingContainer } from "../modules/bookings/entities/booking-container.entity";
|
||||
import { Booking } from "../modules/bookings/entities/booking.entity";
|
||||
import { Customer } from "../modules/customers/entities/customer.entity";
|
||||
import { Locomotive } from "../modules/locomotives/entities/locomotive.entity";
|
||||
import { ServiceType } from "../modules/rule-engine/entities/service-type.entity";
|
||||
import { Yard } from "../modules/rule-engine/entities/yard.entity";
|
||||
import { WagonType } from "../modules/wagon-types/entities/wagon-type.entity";
|
||||
import { ContainerType } from "../modules/rule-engine/entities/container-type.entity";
|
||||
>>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8
|
||||
|
||||
const SEED_FLAG = 'SEED_DEMO_BOOKINGS';
|
||||
const SEED_FLAG = "SEED_DEMO_BOOKINGS";
|
||||
|
||||
const SERVICE_TYPE_CODE = 'RAIL_CONTAINER';
|
||||
const CUSTOMER_EMAIL = 'train-scheduling-demo@edr.local';
|
||||
const SERVICE_TYPE_CODE = "RAIL_CONTAINER";
|
||||
const CUSTOMER_EMAIL = "train-scheduling-demo@edr.local";
|
||||
|
||||
const YARDS = [
|
||||
{ code: 'DJIBOUTI', label: 'Djibouti', country: 'Djibouti', displayOrder: 1 },
|
||||
{ code: 'ADDIS_ABABA', label: 'Addis Ababa', country: 'Ethiopia', displayOrder: 2 },
|
||||
{ code: 'DIRE_DAWA', label: 'Dire Dawa', country: 'Ethiopia', displayOrder: 3 },
|
||||
{ code: "DJIBOUTI", label: "Djibouti", country: "Djibouti", displayOrder: 1 },
|
||||
{
|
||||
code: "ADDIS_ABABA",
|
||||
label: "Addis Ababa",
|
||||
country: "Ethiopia",
|
||||
displayOrder: 2,
|
||||
},
|
||||
{
|
||||
code: "DIRE_DAWA",
|
||||
label: "Dire Dawa",
|
||||
country: "Ethiopia",
|
||||
displayOrder: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const CONTAINER_TYPES = [
|
||||
{ code: '20FT', label: '20FT', sizeFt: 20 },
|
||||
{ code: '40FT', label: '40FT', sizeFt: 40 },
|
||||
{ code: "20FT", label: "20FT", sizeFt: 20 },
|
||||
{ code: "40FT", label: "40FT", sizeFt: 40 },
|
||||
];
|
||||
|
||||
const DEMO_BOOKINGS = [
|
||||
{
|
||||
reference: 'BKG-CONT-001',
|
||||
containerCode: '40FT',
|
||||
reference: "BKG-CONT-001",
|
||||
containerCode: "40FT",
|
||||
quantity: 20,
|
||||
totalWeightTons: 500,
|
||||
originCode: 'DJIBOUTI',
|
||||
destinationCode: 'ADDIS_ABABA',
|
||||
scheduledDate: '2026-06-20T08:00:00.000Z',
|
||||
originCode: "DJIBOUTI",
|
||||
destinationCode: "ADDIS_ABABA",
|
||||
scheduledDate: "2026-06-20T08:00:00.000Z",
|
||||
},
|
||||
{
|
||||
reference: 'BKG-CONT-002',
|
||||
containerCode: '20FT',
|
||||
reference: "BKG-CONT-002",
|
||||
containerCode: "20FT",
|
||||
quantity: 10,
|
||||
totalWeightTons: 300,
|
||||
originCode: 'DJIBOUTI',
|
||||
destinationCode: 'ADDIS_ABABA',
|
||||
scheduledDate: '2026-06-20T08:00:00.000Z',
|
||||
originCode: "DJIBOUTI",
|
||||
destinationCode: "ADDIS_ABABA",
|
||||
scheduledDate: "2026-06-20T08:00:00.000Z",
|
||||
},
|
||||
{
|
||||
reference: 'BKG-CONT-003',
|
||||
containerCode: '40FT',
|
||||
reference: "BKG-CONT-003",
|
||||
containerCode: "40FT",
|
||||
quantity: 15,
|
||||
totalWeightTons: 450,
|
||||
originCode: 'DJIBOUTI',
|
||||
destinationCode: 'ADDIS_ABABA',
|
||||
scheduledDate: '2026-06-20T08:00:00.000Z',
|
||||
originCode: "DJIBOUTI",
|
||||
destinationCode: "ADDIS_ABABA",
|
||||
scheduledDate: "2026-06-20T08:00:00.000Z",
|
||||
},
|
||||
{
|
||||
reference: 'BKG-CONT-007',
|
||||
containerCode: '20FT',
|
||||
reference: "BKG-CONT-007",
|
||||
containerCode: "20FT",
|
||||
quantity: 6,
|
||||
totalWeightTons: 180,
|
||||
originCode: 'DJIBOUTI',
|
||||
destinationCode: 'ADDIS_ABABA',
|
||||
scheduledDate: '2026-06-20T08:00:00.000Z',
|
||||
originCode: "DJIBOUTI",
|
||||
destinationCode: "ADDIS_ABABA",
|
||||
scheduledDate: "2026-06-20T08:00:00.000Z",
|
||||
},
|
||||
{
|
||||
reference: 'BKG-CONT-004',
|
||||
containerCode: '40FT',
|
||||
reference: "BKG-CONT-004",
|
||||
containerCode: "40FT",
|
||||
quantity: 12,
|
||||
totalWeightTons: 360,
|
||||
originCode: 'ADDIS_ABABA',
|
||||
destinationCode: 'DIRE_DAWA',
|
||||
scheduledDate: '2026-06-20T08:00:00.000Z',
|
||||
originCode: "ADDIS_ABABA",
|
||||
destinationCode: "DIRE_DAWA",
|
||||
scheduledDate: "2026-06-20T08:00:00.000Z",
|
||||
},
|
||||
{
|
||||
reference: 'BKG-CONT-005',
|
||||
containerCode: '20FT',
|
||||
reference: "BKG-CONT-005",
|
||||
containerCode: "20FT",
|
||||
quantity: 8,
|
||||
totalWeightTons: 160,
|
||||
originCode: 'DJIBOUTI',
|
||||
destinationCode: 'ADDIS_ABABA',
|
||||
scheduledDate: '2026-06-21T08:00:00.000Z',
|
||||
originCode: "DJIBOUTI",
|
||||
destinationCode: "ADDIS_ABABA",
|
||||
scheduledDate: "2026-06-21T08:00:00.000Z",
|
||||
},
|
||||
{
|
||||
reference: 'BKG-CONT-006',
|
||||
containerCode: '40FT',
|
||||
reference: "BKG-CONT-006",
|
||||
containerCode: "40FT",
|
||||
quantity: 80,
|
||||
totalWeightTons: 3600,
|
||||
originCode: 'DJIBOUTI',
|
||||
destinationCode: 'ADDIS_ABABA',
|
||||
scheduledDate: '2026-06-20T08:00:00.000Z',
|
||||
originCode: "DJIBOUTI",
|
||||
destinationCode: "ADDIS_ABABA",
|
||||
scheduledDate: "2026-06-20T08:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -97,24 +118,26 @@ const DEMO_BOOKINGS = [
|
||||
export class DemoBookingsSeeder {
|
||||
private readonly logger = new Logger(DemoBookingsSeeder.name);
|
||||
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
constructor(private readonly dataSource: DataSource) { }
|
||||
|
||||
async run() {
|
||||
const shouldSeed = process.env[SEED_FLAG]?.trim().toLowerCase() === 'true';
|
||||
const shouldSeed = process.env[SEED_FLAG]?.trim().toLowerCase() === "true";
|
||||
if (!shouldSeed) {
|
||||
this.logger.log(`Skipping demo booking seed because ${SEED_FLAG} is not enabled`);
|
||||
this.logger.log(
|
||||
`Skipping demo booking seed because ${SEED_FLAG} is not enabled`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await manager.getRepository(WagonType).upsert(
|
||||
{
|
||||
code: 'NW5',
|
||||
name: 'Flat Wagon',
|
||||
code: "NW5",
|
||||
name: "Flat Wagon",
|
||||
capacityTons: 70,
|
||||
lengthMeters: 14,
|
||||
maxWagonsPerTrain: 53,
|
||||
supportedLoadTypes: ['CONTAINER'],
|
||||
supportedLoadTypes: ["CONTAINER"],
|
||||
isActive: true,
|
||||
},
|
||||
{ conflictPaths: { code: true } },
|
||||
@@ -123,16 +146,16 @@ export class DemoBookingsSeeder {
|
||||
await manager.getRepository(Locomotive).upsert(
|
||||
[
|
||||
{
|
||||
code: 'LOC-001',
|
||||
name: 'Demo Locomotive 1',
|
||||
code: "LOC-001",
|
||||
name: "Demo Locomotive 1",
|
||||
maxPullWeightTons: 3500,
|
||||
status: 'AVAILABLE',
|
||||
status: "AVAILABLE",
|
||||
},
|
||||
{
|
||||
code: 'LOC-002',
|
||||
name: 'Demo Locomotive 2',
|
||||
code: "LOC-002",
|
||||
name: "Demo Locomotive 2",
|
||||
maxPullWeightTons: 2500,
|
||||
status: 'AVAILABLE',
|
||||
status: "AVAILABLE",
|
||||
},
|
||||
],
|
||||
{ conflictPaths: { code: true } },
|
||||
@@ -146,8 +169,8 @@ export class DemoBookingsSeeder {
|
||||
await manager.getRepository(ServiceType).upsert(
|
||||
{
|
||||
code: SERVICE_TYPE_CODE,
|
||||
serviceName: 'Rail Container Service',
|
||||
description: 'Temporary service type for train scheduling demos',
|
||||
serviceName: "Rail Container Service",
|
||||
description: "Temporary service type for train scheduling demos",
|
||||
canBeBookedAlone: true,
|
||||
includesFirstMile: false,
|
||||
includesLastMile: false,
|
||||
@@ -173,74 +196,97 @@ export class DemoBookingsSeeder {
|
||||
|
||||
await manager.getRepository(Customer).upsert(
|
||||
{
|
||||
userId: '00000000-0000-0000-0000-000000000111',
|
||||
firstName: 'Train',
|
||||
lastName: 'Scheduling',
|
||||
userId: "00000000-0000-0000-0000-000000000111",
|
||||
firstName: "Train",
|
||||
lastName: "Scheduling",
|
||||
email: CUSTOMER_EMAIL,
|
||||
phone: '251900000001',
|
||||
companyName: 'Train Scheduling Demo Customer',
|
||||
phone: "251900000001",
|
||||
companyName: "Train Scheduling Demo Customer",
|
||||
companyEmail: CUSTOMER_EMAIL,
|
||||
companyPhone: '251900000001',
|
||||
companyLocation: 'Addis Ababa',
|
||||
companyAddress: 'Demo Address',
|
||||
customerType: 'DEMO',
|
||||
status: 'ACTIVE',
|
||||
contactPersonName: 'Train Scheduling',
|
||||
contactPersonPhone: '251900000001',
|
||||
tinNumber: '1234567890',
|
||||
vatNumber: '1234567890',
|
||||
fanNumber: '1234567890123456',
|
||||
generalManagerName: 'Demo Manager',
|
||||
companyPhone: "251900000001",
|
||||
companyLocation: "Addis Ababa",
|
||||
companyAddress: "Demo Address",
|
||||
customerType: "DEMO",
|
||||
status: "ACTIVE",
|
||||
contactPersonName: "Train Scheduling",
|
||||
contactPersonPhone: "251900000001",
|
||||
tinNumber: "1234567890",
|
||||
vatNumber: "1234567890",
|
||||
fanNumber: "1234567890123456",
|
||||
generalManagerName: "Demo Manager",
|
||||
generalManagerEmail: CUSTOMER_EMAIL,
|
||||
generalManagerPhone: '251900000001',
|
||||
generalManagerPhone: "251900000001",
|
||||
},
|
||||
{ conflictPaths: { email: true } },
|
||||
);
|
||||
|
||||
<<<<<<< HEAD
|
||||
const [serviceType, company, yards, containerTypes] = await Promise.all([
|
||||
manager.getRepository(ServiceType).findOneByOrFail({ code: SERVICE_TYPE_CODE }),
|
||||
manager.getRepository(Customer).findOneByOrFail({ email: CUSTOMER_EMAIL }),
|
||||
=======
|
||||
const [serviceType, customer, yards, containerTypes] = await Promise.all([
|
||||
manager
|
||||
.getRepository(ServiceType)
|
||||
.findOneByOrFail({ code: SERVICE_TYPE_CODE }),
|
||||
manager
|
||||
.getRepository(Customer)
|
||||
.findOneByOrFail({ email: CUSTOMER_EMAIL }),
|
||||
>>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8
|
||||
manager.getRepository(Yard).find(),
|
||||
manager.getRepository(ContainerType).find(),
|
||||
]);
|
||||
|
||||
const yardByCode = new Map(yards.map((yard) => [yard.code, yard]));
|
||||
const containerTypeByCode = new Map(
|
||||
containerTypes.map((containerType) => [containerType.code, containerType]),
|
||||
containerTypes.map((containerType) => [
|
||||
containerType.code,
|
||||
containerType,
|
||||
]),
|
||||
);
|
||||
|
||||
for (const demoBooking of DEMO_BOOKINGS) {
|
||||
const origin = yardByCode.get(demoBooking.originCode);
|
||||
const destination = yardByCode.get(demoBooking.destinationCode);
|
||||
const containerType = containerTypeByCode.get(demoBooking.containerCode);
|
||||
const containerType = containerTypeByCode.get(
|
||||
demoBooking.containerCode,
|
||||
);
|
||||
|
||||
if (!origin || !destination || !containerType) {
|
||||
throw new Error(`demo_booking_seed_dependency_missing:${demoBooking.reference}`);
|
||||
throw new Error(
|
||||
`demo_booking_seed_dependency_missing:${demoBooking.reference}`,
|
||||
);
|
||||
}
|
||||
|
||||
const vgmPerUnitTons = demoBooking.totalWeightTons / demoBooking.quantity;
|
||||
const vgmPerUnitTons =
|
||||
demoBooking.totalWeightTons / demoBooking.quantity;
|
||||
|
||||
await manager.getRepository(Booking).upsert(
|
||||
{
|
||||
reference: demoBooking.reference,
|
||||
<<<<<<< HEAD
|
||||
companyId: company.id,
|
||||
status: 'APPROVED',
|
||||
=======
|
||||
companyId: customer.id,
|
||||
status: "APPROVED",
|
||||
>>>>>>> bd6ad54eea229305a214d06081e0ffd27fe163b8
|
||||
scheduledDate: new Date(demoBooking.scheduledDate),
|
||||
totalAmount: 0,
|
||||
paymentStatus: 'PENDING',
|
||||
contractType: 'NEW',
|
||||
paymentStatus: "PENDING",
|
||||
contractType: "NEW",
|
||||
serviceTypeId: serviceType.id,
|
||||
equipmentReturn: 'WITHOUT_RETURN',
|
||||
equipmentReturn: "WITHOUT_RETURN",
|
||||
originYardId: origin.id,
|
||||
destinationYardId: destination.id,
|
||||
tradeDirection: 'IMPORT',
|
||||
freightType: 'CONTAINER',
|
||||
tradeDirection: "IMPORT",
|
||||
freightType: "CONTAINER",
|
||||
cargoTypeId: null,
|
||||
cargoFreeText: null,
|
||||
shippingLineId: null,
|
||||
cargoTotalWeightVgm: demoBooking.totalWeightTons,
|
||||
isHazardous: false,
|
||||
paymentCurrency: 'USD',
|
||||
paymentCurrency: "USD",
|
||||
allowConsolidation: false,
|
||||
priorityScore: 0,
|
||||
versionNumber: 1,
|
||||
@@ -252,7 +298,9 @@ export class DemoBookingsSeeder {
|
||||
reference: demoBooking.reference,
|
||||
});
|
||||
|
||||
await manager.getRepository(BookingContainer).delete({ bookingId: booking.id });
|
||||
await manager
|
||||
.getRepository(BookingContainer)
|
||||
.delete({ bookingId: booking.id });
|
||||
await manager.getRepository(BookingContainer).insert({
|
||||
id: randomUUID(),
|
||||
bookingId: booking.id,
|
||||
@@ -264,11 +312,13 @@ export class DemoBookingsSeeder {
|
||||
weightLimitRuleId: null,
|
||||
isOverweight: demoBooking.totalWeightTons > 70,
|
||||
overweightExcessTons:
|
||||
demoBooking.totalWeightTons > 70 ? demoBooking.totalWeightTons - 70 : null,
|
||||
demoBooking.totalWeightTons > 70
|
||||
? demoBooking.totalWeightTons - 70
|
||||
: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.log('Seeded demo train scheduling data');
|
||||
this.logger.log("Seeded demo train scheduling data");
|
||||
}
|
||||
}
|
||||
|
||||
800
apps/edr-freight-api/src/seed/pricing-data.seeder.ts
Normal file
800
apps/edr-freight-api/src/seed/pricing-data.seeder.ts
Normal file
@@ -0,0 +1,800 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { DataSource } from "typeorm";
|
||||
|
||||
import { BookingCargoModifier } from "../modules/bookings/entities/booking-cargo-modifier.entity";
|
||||
import { CargoType } from "../modules/rule-engine/entities/cargo-type.entity";
|
||||
import { ContainerType } from "../modules/rule-engine/entities/container-type.entity";
|
||||
import { PriorityRule } from "../modules/rule-engine/entities/priority-rule.entity";
|
||||
import { Rate } from "../modules/rule-engine/entities/rate.entity";
|
||||
import { ServiceType } from "../modules/rule-engine/entities/service-type.entity";
|
||||
import { ShippingLine } from "../modules/rule-engine/entities/shipping-line.entity";
|
||||
import { SurchargeType } from "../modules/rule-engine/entities/surcharge-type.entity";
|
||||
import { WeightLimitRule } from "../modules/rule-engine/entities/weight-limit-rule.entity";
|
||||
import { Yard } from "../modules/rule-engine/entities/yard.entity";
|
||||
|
||||
const STAFF_USER_ID = "00000000-0000-0000-0000-000000000001";
|
||||
const CEO_USER_ID = "00000000-0000-0000-0000-000000000002";
|
||||
|
||||
@Injectable()
|
||||
export class PricingDataSeeder {
|
||||
private readonly logger = new Logger(PricingDataSeeder.name);
|
||||
|
||||
constructor(private readonly dataSource: DataSource) { }
|
||||
|
||||
async run(): Promise<void> {
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const ctRepo = manager.getRepository(ContainerType);
|
||||
const stRepo = manager.getRepository(ServiceType);
|
||||
const yRepo = manager.getRepository(Yard);
|
||||
const slRepo = manager.getRepository(ShippingLine);
|
||||
const wlRepo = manager.getRepository(WeightLimitRule);
|
||||
const prRepo = manager.getRepository(PriorityRule);
|
||||
const rRepo = manager.getRepository(Rate);
|
||||
|
||||
await this.upsertReferenceData(manager, ctRepo, stRepo, yRepo, slRepo);
|
||||
await this.seedWeightLimits(wlRepo, ctRepo);
|
||||
await this.seedPriorityRules(prRepo);
|
||||
const containerTypes = await ctRepo.find();
|
||||
const ctByCode = new Map(containerTypes.map((ct) => [ct.code, ct]));
|
||||
|
||||
const rates = await this.seedRates(rRepo, ctByCode);
|
||||
const ratesByType = new Map<string, Rate[]>();
|
||||
for (const r of rates) {
|
||||
const key = `${r.rateType}|${r.currency}|${r.containerTypeId ?? ""}`;
|
||||
if (!ratesByType.has(key)) ratesByType.set(key, []);
|
||||
ratesByType.get(key)!.push(r);
|
||||
}
|
||||
|
||||
await this.seedSurchargeTypes(manager, ratesByType);
|
||||
|
||||
const yards = await yRepo.find();
|
||||
const yardByCode = new Map(yards.map((y) => [y.code, y]));
|
||||
const serviceTypes = await stRepo.find();
|
||||
const stByCode = new Map(serviceTypes.map((st) => [st.code, st]));
|
||||
const shippingLines = await slRepo.find();
|
||||
const slByCode = new Map(shippingLines.map((sl) => [sl.code, sl]));
|
||||
const cargoTypes = await manager.getRepository(CargoType).find();
|
||||
const cargoByCode = new Map(cargoTypes.map((c) => [c.code, c]));
|
||||
|
||||
await this.seedDraftBookings(
|
||||
ctByCode,
|
||||
yardByCode,
|
||||
stByCode,
|
||||
slByCode,
|
||||
cargoByCode,
|
||||
);
|
||||
});
|
||||
|
||||
this.logger.log("Seeded pricing data");
|
||||
}
|
||||
|
||||
private async upsertReferenceData(
|
||||
manager: any,
|
||||
ctRepo: any,
|
||||
stRepo: any,
|
||||
yRepo: any,
|
||||
slRepo: any,
|
||||
): Promise<void> {
|
||||
await yRepo.upsert(
|
||||
[
|
||||
{
|
||||
code: "DJIBOUTI",
|
||||
label: "Djibouti",
|
||||
country: "Djibouti",
|
||||
displayOrder: 1,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
code: "ADDIS_ABABA",
|
||||
label: "Addis Ababa",
|
||||
country: "Ethiopia",
|
||||
displayOrder: 2,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
code: "DIRE_DAWA",
|
||||
label: "Dire Dawa",
|
||||
country: "Ethiopia",
|
||||
displayOrder: 3,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
code: "MODJO",
|
||||
label: "Modjo",
|
||||
country: "Ethiopia",
|
||||
displayOrder: 4,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
{ conflictPaths: { code: true } },
|
||||
);
|
||||
|
||||
await ctRepo.upsert(
|
||||
[
|
||||
{
|
||||
code: "20FT",
|
||||
label: "20FT Standard",
|
||||
sizeFt: 20,
|
||||
wagonsPerUnit: 1,
|
||||
isReefer: false,
|
||||
isOpenTop: false,
|
||||
isActive: true,
|
||||
displayOrder: 1,
|
||||
},
|
||||
{
|
||||
code: "40FT",
|
||||
label: "40FT Standard",
|
||||
sizeFt: 40,
|
||||
wagonsPerUnit: 1,
|
||||
isReefer: false,
|
||||
isOpenTop: false,
|
||||
isActive: true,
|
||||
displayOrder: 2,
|
||||
},
|
||||
{
|
||||
code: "20FT_REEFER",
|
||||
label: "20FT Reefer",
|
||||
sizeFt: 20,
|
||||
wagonsPerUnit: 1,
|
||||
isReefer: true,
|
||||
isOpenTop: false,
|
||||
isActive: true,
|
||||
displayOrder: 3,
|
||||
},
|
||||
{
|
||||
code: "40FT_REEFER",
|
||||
label: "40FT Reefer",
|
||||
sizeFt: 40,
|
||||
wagonsPerUnit: 1,
|
||||
isReefer: true,
|
||||
isOpenTop: false,
|
||||
isActive: true,
|
||||
displayOrder: 4,
|
||||
},
|
||||
],
|
||||
{ conflictPaths: { code: true } },
|
||||
);
|
||||
|
||||
await stRepo.upsert(
|
||||
[
|
||||
{
|
||||
code: "RAIL_CONTAINER",
|
||||
serviceName: "Rail Container Service",
|
||||
description: "Standard rail container transport",
|
||||
canBeBookedAlone: true,
|
||||
includesFirstMile: false,
|
||||
includesLastMile: false,
|
||||
includesCustoms: false,
|
||||
priorityBonusPoints: 0,
|
||||
isActive: true,
|
||||
displayOrder: 1,
|
||||
},
|
||||
{
|
||||
code: "RAIL_FORWARDING",
|
||||
serviceName: "Rail Forwarding Service",
|
||||
description: "Rail transport with first/last mile and customs",
|
||||
canBeBookedAlone: true,
|
||||
includesFirstMile: true,
|
||||
includesLastMile: true,
|
||||
includesCustoms: true,
|
||||
priorityBonusPoints: 100,
|
||||
isActive: true,
|
||||
displayOrder: 2,
|
||||
},
|
||||
{
|
||||
code: "RAIL_BULK",
|
||||
serviceName: "Rail Bulk Transport",
|
||||
description: "Bulk commodity rail transport",
|
||||
canBeBookedAlone: true,
|
||||
includesFirstMile: false,
|
||||
includesLastMile: false,
|
||||
includesCustoms: false,
|
||||
priorityBonusPoints: 50,
|
||||
isActive: true,
|
||||
displayOrder: 3,
|
||||
},
|
||||
],
|
||||
{ conflictPaths: { code: true } },
|
||||
);
|
||||
|
||||
await slRepo.upsert(
|
||||
[
|
||||
{
|
||||
code: "MAERSK",
|
||||
label: "Maersk Line",
|
||||
mappedToCode: "MAERSK",
|
||||
showExtraFeeNotice: true,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
code: "MSC",
|
||||
label: "MSC",
|
||||
mappedToCode: "MSC",
|
||||
showExtraFeeNotice: true,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
code: "CMA_CGM",
|
||||
label: "CMA CGM",
|
||||
mappedToCode: "CMA_CGM",
|
||||
showExtraFeeNotice: true,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
code: "COSCO",
|
||||
label: "COSCO Shipping",
|
||||
mappedToCode: "COSCO",
|
||||
showExtraFeeNotice: true,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
code: "OTHER",
|
||||
label: "Other Line",
|
||||
mappedToCode: null,
|
||||
showExtraFeeNotice: false,
|
||||
isActive: true,
|
||||
},
|
||||
],
|
||||
{ conflictPaths: { code: true } },
|
||||
);
|
||||
|
||||
await manager.getRepository(CargoType).upsert(
|
||||
[
|
||||
{
|
||||
code: "GRAIN",
|
||||
cargoTypeName: "Grain / Cereals",
|
||||
requiresDirectorApproval: false,
|
||||
isActive: true,
|
||||
displayOrder: 1,
|
||||
},
|
||||
{
|
||||
code: "FERTILIZER",
|
||||
cargoTypeName: "Fertilizer",
|
||||
requiresDirectorApproval: false,
|
||||
isActive: true,
|
||||
displayOrder: 2,
|
||||
},
|
||||
{
|
||||
code: "CEMENT",
|
||||
cargoTypeName: "Cement / Clinker",
|
||||
requiresDirectorApproval: false,
|
||||
isActive: true,
|
||||
displayOrder: 3,
|
||||
},
|
||||
{
|
||||
code: "STEEL",
|
||||
cargoTypeName: "Steel / Rebar",
|
||||
requiresDirectorApproval: true,
|
||||
isActive: true,
|
||||
displayOrder: 4,
|
||||
},
|
||||
{
|
||||
code: "MACHINERY",
|
||||
cargoTypeName: "Heavy Machinery",
|
||||
requiresDirectorApproval: true,
|
||||
isActive: true,
|
||||
displayOrder: 5,
|
||||
},
|
||||
{
|
||||
code: "OTHER_BULK",
|
||||
cargoTypeName: "Other Bulk Cargo",
|
||||
requiresDirectorApproval: false,
|
||||
isActive: true,
|
||||
displayOrder: 6,
|
||||
},
|
||||
],
|
||||
{ conflictPaths: { code: true } },
|
||||
);
|
||||
}
|
||||
|
||||
private async seedWeightLimits(wlRepo: any, ctRepo: any): Promise<void> {
|
||||
await wlRepo.createQueryBuilder().delete().execute();
|
||||
const twenty = await ctRepo.findOneByOrFail({ code: "20FT" });
|
||||
const forty = await ctRepo.findOneByOrFail({ code: "40FT" });
|
||||
const base = new Date("2026-01-01");
|
||||
await wlRepo.insert([
|
||||
{
|
||||
containerTypeId: twenty.id,
|
||||
tradeDirection: "IMPORT",
|
||||
maxVgmTons: 26,
|
||||
effectiveFrom: base,
|
||||
},
|
||||
{
|
||||
containerTypeId: twenty.id,
|
||||
tradeDirection: "EXPORT",
|
||||
maxVgmTons: 26,
|
||||
effectiveFrom: base,
|
||||
},
|
||||
{
|
||||
containerTypeId: forty.id,
|
||||
tradeDirection: "IMPORT",
|
||||
maxVgmTons: 28,
|
||||
effectiveFrom: base,
|
||||
},
|
||||
{
|
||||
containerTypeId: forty.id,
|
||||
tradeDirection: "EXPORT",
|
||||
maxVgmTons: 28,
|
||||
effectiveFrom: base,
|
||||
},
|
||||
]);
|
||||
this.logger.log("Seeded weight limit rules");
|
||||
}
|
||||
|
||||
private async seedPriorityRules(prRepo: any): Promise<void> {
|
||||
const existing = await prRepo.find({
|
||||
where: [{ code: "USD_PRIORITY" }, { code: "STANDARD_PRIORITY" }],
|
||||
});
|
||||
for (const r of existing) {
|
||||
await prRepo.remove(r);
|
||||
}
|
||||
await prRepo.save([
|
||||
prRepo.create({
|
||||
code: "USD_PRIORITY",
|
||||
label: "USD Payment Priority",
|
||||
score: 200,
|
||||
conditionCurrency: "USD",
|
||||
isActive: true,
|
||||
}),
|
||||
prRepo.create({
|
||||
code: "STANDARD_PRIORITY",
|
||||
label: "Standard Priority",
|
||||
score: 50,
|
||||
conditionCurrency: null,
|
||||
isActive: true,
|
||||
}),
|
||||
]);
|
||||
this.logger.log("Seeded priority rules");
|
||||
}
|
||||
|
||||
private async seedRates(
|
||||
rRepo: any,
|
||||
ctByCode: Map<string, any>,
|
||||
): Promise<Rate[]> {
|
||||
const effectiveFrom = new Date("2026-01-01");
|
||||
const now = new Date();
|
||||
const rateData = [
|
||||
{
|
||||
rateType: "CONTAINER_IMPORT",
|
||||
containerTypeId: ctByCode.get("20FT")!.id,
|
||||
currency: "USD",
|
||||
rateValue: 800,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_IMPORT",
|
||||
containerTypeId: ctByCode.get("40FT")!.id,
|
||||
currency: "USD",
|
||||
rateValue: 1200,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_IMPORT",
|
||||
containerTypeId: ctByCode.get("20FT")!.id,
|
||||
currency: "ETB",
|
||||
rateValue: 45000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_IMPORT",
|
||||
containerTypeId: ctByCode.get("40FT")!.id,
|
||||
currency: "ETB",
|
||||
rateValue: 67000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_EXPORT",
|
||||
containerTypeId: ctByCode.get("20FT")!.id,
|
||||
currency: "USD",
|
||||
rateValue: 600,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_EXPORT",
|
||||
containerTypeId: ctByCode.get("40FT")!.id,
|
||||
currency: "USD",
|
||||
rateValue: 900,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_EXPORT",
|
||||
containerTypeId: ctByCode.get("20FT")!.id,
|
||||
currency: "ETB",
|
||||
rateValue: 34000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_EXPORT",
|
||||
containerTypeId: ctByCode.get("40FT")!.id,
|
||||
currency: "ETB",
|
||||
rateValue: 50000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "INTERCITY_CONTAINER",
|
||||
containerTypeId: ctByCode.get("20FT")!.id,
|
||||
currency: "ETB",
|
||||
rateValue: 20000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "INTERCITY_CONTAINER",
|
||||
containerTypeId: ctByCode.get("40FT")!.id,
|
||||
currency: "ETB",
|
||||
rateValue: 30000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_IMPORT",
|
||||
containerTypeId: null,
|
||||
currency: "USD",
|
||||
rateValue: 1000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_IMPORT",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 56000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_EXPORT",
|
||||
containerTypeId: null,
|
||||
currency: "USD",
|
||||
rateValue: 750,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_EXPORT",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 42000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "INTERCITY_CONTAINER",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 25000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "BULK_IMPORT",
|
||||
containerTypeId: null,
|
||||
currency: "USD",
|
||||
rateValue: 50,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "BULK_IMPORT",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 2800,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "BULK_EXPORT",
|
||||
containerTypeId: null,
|
||||
currency: "USD",
|
||||
rateValue: 40,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "BULK_EXPORT",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 2200,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "OVERWEIGHT_PER_TON",
|
||||
containerTypeId: null,
|
||||
currency: "USD",
|
||||
rateValue: 25,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "OVERWEIGHT_PER_TON",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 1400,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "HAZARD_SURCHARGE",
|
||||
containerTypeId: null,
|
||||
currency: "USD",
|
||||
rateValue: 150,
|
||||
rateUnit: "FLAT",
|
||||
},
|
||||
{
|
||||
rateType: "HAZARD_SURCHARGE",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 8500,
|
||||
rateUnit: "FLAT",
|
||||
},
|
||||
{
|
||||
rateType: "REEFER_SURCHARGE",
|
||||
containerTypeId: null,
|
||||
currency: "USD",
|
||||
rateValue: 200,
|
||||
rateUnit: "FLAT",
|
||||
},
|
||||
{
|
||||
rateType: "REEFER_SURCHARGE",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 11000,
|
||||
rateUnit: "FLAT",
|
||||
},
|
||||
{
|
||||
rateType: "DOUBLE_HANDLING",
|
||||
containerTypeId: null,
|
||||
currency: "USD",
|
||||
rateValue: 100,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "DOUBLE_HANDLING",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 5500,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "LASHING",
|
||||
containerTypeId: null,
|
||||
currency: "USD",
|
||||
rateValue: 50,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "LASHING",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 2800,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
];
|
||||
|
||||
const entities = rateData.map((d) =>
|
||||
rRepo.create({
|
||||
...d,
|
||||
status: "LIVE",
|
||||
proposedByStaffId: STAFF_USER_ID,
|
||||
approvedByCeoId: CEO_USER_ID,
|
||||
approvedAt: now,
|
||||
effectiveFrom,
|
||||
}),
|
||||
);
|
||||
return rRepo.save(entities);
|
||||
}
|
||||
|
||||
private async seedSurchargeTypes(
|
||||
manager: any,
|
||||
ratesByType: Map<string, Rate[]>,
|
||||
): Promise<void> {
|
||||
const surRepo = manager.getRepository(SurchargeType);
|
||||
const bcmRepo = manager.getRepository(BookingCargoModifier);
|
||||
await bcmRepo.createQueryBuilder().delete().execute();
|
||||
const findRate = (rateType: string, currency: string) => {
|
||||
const key = `${rateType}|${currency}|`;
|
||||
const rates = ratesByType.get(key);
|
||||
return rates?.[0];
|
||||
};
|
||||
|
||||
const hazardRateUsd = findRate("HAZARD_SURCHARGE", "USD");
|
||||
const hazardRateEtb = findRate("HAZARD_SURCHARGE", "ETB");
|
||||
const reeferRateUsd = findRate("REEFER_SURCHARGE", "USD");
|
||||
const reeferRateEtb = findRate("REEFER_SURCHARGE", "ETB");
|
||||
const overweightRateUsd = findRate("OVERWEIGHT_PER_TON", "USD");
|
||||
const overweightRateEtb = findRate("OVERWEIGHT_PER_TON", "ETB");
|
||||
const shipLineRateUsd = findRate("DOUBLE_HANDLING", "USD");
|
||||
const shipLineRateEtb = findRate("DOUBLE_HANDLING", "ETB");
|
||||
const consolidRateUsd = findRate("LASHING", "USD");
|
||||
const consolidRateEtb = findRate("LASHING", "ETB");
|
||||
|
||||
await surRepo.createQueryBuilder().delete().execute();
|
||||
await surRepo.save([
|
||||
surRepo.create({
|
||||
code: "HAZARDOUS_CARGO",
|
||||
label: "Hazardous Cargo",
|
||||
triggerCondition: "CARGO_FLAG_HAZARDOUS",
|
||||
rateId: hazardRateUsd?.id ?? hazardRateEtb?.id,
|
||||
isActive: true,
|
||||
}),
|
||||
surRepo.create({
|
||||
code: "REEFER_CARGO",
|
||||
label: "Reefer Cargo",
|
||||
triggerCondition: "CARGO_FLAG_REEFER",
|
||||
rateId: reeferRateUsd?.id ?? reeferRateEtb?.id,
|
||||
isActive: true,
|
||||
}),
|
||||
surRepo.create({
|
||||
code: "OVERWEIGHT_CARGO",
|
||||
label: "Overweight Cargo",
|
||||
triggerCondition: "VGM_EXCEEDS_LIMIT",
|
||||
rateId: overweightRateUsd?.id ?? overweightRateEtb?.id,
|
||||
isActive: true,
|
||||
}),
|
||||
surRepo.create({
|
||||
code: "SHIPPING_LINE_FEE",
|
||||
label: "Shipping Line Fee",
|
||||
triggerCondition: "SHIPPING_LINE_MAPPED",
|
||||
rateId: shipLineRateUsd?.id ?? shipLineRateEtb?.id,
|
||||
isActive: true,
|
||||
}),
|
||||
surRepo.create({
|
||||
code: "CONSOLIDATION_FEE",
|
||||
label: "Consolidation Fee",
|
||||
triggerCondition: "CONSOLIDATION_ENABLED",
|
||||
rateId: consolidRateUsd?.id ?? consolidRateEtb?.id,
|
||||
isActive: true,
|
||||
}),
|
||||
]);
|
||||
this.logger.log("Seeded surcharge types");
|
||||
}
|
||||
|
||||
private async seedDraftBookings(
|
||||
ctByCode: Map<string, any>,
|
||||
yardByCode: Map<string, any>,
|
||||
stByCode: Map<string, any>,
|
||||
slByCode: Map<string, any>,
|
||||
cargoByCode: Map<string, any>,
|
||||
): Promise<void> {
|
||||
const djibouti = yardByCode.get("DJIBOUTI")!;
|
||||
const addis = yardByCode.get("ADDIS_ABABA")!;
|
||||
const railContainer = stByCode.get("RAIL_CONTAINER")!;
|
||||
const railBulk = stByCode.get("RAIL_BULK")!;
|
||||
const maersk = slByCode.get("MAERSK")!;
|
||||
const grain = cargoByCode.get("GRAIN")!;
|
||||
const twenty = ctByCode.get("20FT")!;
|
||||
const forty = ctByCode.get("40FT")!;
|
||||
const twentyReefer = ctByCode.get("20FT_REEFER")!;
|
||||
|
||||
const drafts = [
|
||||
{
|
||||
reference: "BKG-PRICE-001",
|
||||
description: "Standard 20FT container import — base rail only",
|
||||
freightType: "CONTAINER" as const,
|
||||
tradeDirection: "IMPORT",
|
||||
paymentCurrency: "USD",
|
||||
serviceTypeId: railContainer.id,
|
||||
originYardId: djibouti.id,
|
||||
destinationYardId: addis.id,
|
||||
isHazardous: false,
|
||||
allowConsolidation: false,
|
||||
shippingLineId: null,
|
||||
cargoTypeId: null,
|
||||
cargoTotalWeightVgm: 250,
|
||||
containers: [
|
||||
{ containerTypeId: twenty.id, quantity: 10, vgmPerUnitTons: 25 },
|
||||
],
|
||||
expectedBaseRate: 800,
|
||||
expectedSurcharges: [],
|
||||
},
|
||||
{
|
||||
reference: "BKG-PRICE-002",
|
||||
description: "40FT container import + hazardous surcharge",
|
||||
freightType: "CONTAINER" as const,
|
||||
tradeDirection: "IMPORT",
|
||||
paymentCurrency: "USD",
|
||||
serviceTypeId: railContainer.id,
|
||||
originYardId: djibouti.id,
|
||||
destinationYardId: addis.id,
|
||||
isHazardous: true,
|
||||
allowConsolidation: false,
|
||||
shippingLineId: null,
|
||||
cargoTypeId: null,
|
||||
cargoTotalWeightVgm: 135,
|
||||
containers: [
|
||||
{ containerTypeId: forty.id, quantity: 5, vgmPerUnitTons: 27 },
|
||||
],
|
||||
expectedBaseRate: 1200,
|
||||
expectedSurcharges: ["HAZARDOUS_CARGO"],
|
||||
},
|
||||
{
|
||||
reference: "BKG-PRICE-003",
|
||||
description: "20FT container import + shipping line (ETB)",
|
||||
freightType: "CONTAINER" as const,
|
||||
tradeDirection: "IMPORT",
|
||||
paymentCurrency: "ETB",
|
||||
serviceTypeId: railContainer.id,
|
||||
originYardId: djibouti.id,
|
||||
destinationYardId: addis.id,
|
||||
isHazardous: false,
|
||||
allowConsolidation: false,
|
||||
shippingLineId: maersk.id,
|
||||
cargoTypeId: null,
|
||||
cargoTotalWeightVgm: 480,
|
||||
containers: [
|
||||
{ containerTypeId: twenty.id, quantity: 20, vgmPerUnitTons: 24 },
|
||||
],
|
||||
expectedBaseRate: 45000,
|
||||
expectedSurcharges: ["SHIPPING_LINE_FEE"],
|
||||
},
|
||||
{
|
||||
reference: "BKG-PRICE-004",
|
||||
description: "40FT container import + consolidation (USD)",
|
||||
freightType: "CONTAINER" as const,
|
||||
tradeDirection: "IMPORT",
|
||||
paymentCurrency: "USD",
|
||||
serviceTypeId: railContainer.id,
|
||||
originYardId: djibouti.id,
|
||||
destinationYardId: addis.id,
|
||||
isHazardous: false,
|
||||
allowConsolidation: true,
|
||||
shippingLineId: null,
|
||||
cargoTypeId: null,
|
||||
cargoTotalWeightVgm: 224,
|
||||
containers: [
|
||||
{ containerTypeId: forty.id, quantity: 8, vgmPerUnitTons: 28 },
|
||||
],
|
||||
expectedBaseRate: 1200,
|
||||
expectedSurcharges: ["CONSOLIDATION_FEE"],
|
||||
},
|
||||
{
|
||||
reference: "BKG-PRICE-005",
|
||||
description: "Bulk import — grain",
|
||||
freightType: "BULK" as const,
|
||||
tradeDirection: "IMPORT",
|
||||
paymentCurrency: "USD",
|
||||
serviceTypeId: railBulk.id,
|
||||
originYardId: djibouti.id,
|
||||
destinationYardId: addis.id,
|
||||
isHazardous: false,
|
||||
allowConsolidation: false,
|
||||
shippingLineId: null,
|
||||
cargoTypeId: grain.id,
|
||||
cargoTotalWeightVgm: 500,
|
||||
containers: [],
|
||||
expectedBaseRate: 50,
|
||||
expectedSurcharges: [],
|
||||
},
|
||||
{
|
||||
reference: "BKG-PRICE-006",
|
||||
description: "20FT reefer container import + reefer surcharge",
|
||||
freightType: "CONTAINER" as const,
|
||||
tradeDirection: "IMPORT",
|
||||
paymentCurrency: "USD",
|
||||
serviceTypeId: railContainer.id,
|
||||
originYardId: djibouti.id,
|
||||
destinationYardId: addis.id,
|
||||
isHazardous: false,
|
||||
allowConsolidation: false,
|
||||
shippingLineId: null,
|
||||
cargoTypeId: null,
|
||||
cargoTotalWeightVgm: 75,
|
||||
containers: [
|
||||
{ containerTypeId: twentyReefer.id, quantity: 3, vgmPerUnitTons: 25 },
|
||||
],
|
||||
expectedBaseRate: 800,
|
||||
expectedSurcharges: ["REEFER_CARGO"],
|
||||
},
|
||||
{
|
||||
reference: "BKG-PRICE-007",
|
||||
description: "20FT container import + overweight (30t > 26t limit)",
|
||||
freightType: "CONTAINER" as const,
|
||||
tradeDirection: "IMPORT",
|
||||
paymentCurrency: "USD",
|
||||
serviceTypeId: railContainer.id,
|
||||
originYardId: djibouti.id,
|
||||
destinationYardId: addis.id,
|
||||
isHazardous: false,
|
||||
allowConsolidation: false,
|
||||
shippingLineId: null,
|
||||
cargoTypeId: null,
|
||||
cargoTotalWeightVgm: 300,
|
||||
containers: [
|
||||
{ containerTypeId: twenty.id, quantity: 10, vgmPerUnitTons: 30 },
|
||||
],
|
||||
expectedBaseRate: 800,
|
||||
expectedSurcharges: ["OVERWEIGHT_CARGO"],
|
||||
},
|
||||
];
|
||||
|
||||
this.logger.log(`Seeded ${drafts.length} DRAFT bookings for pricing`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user