mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 12:41:04 +00:00
Batch 1 to 3 implementation
This commit is contained in:
0
.pnpm-store/v11/.pnpm-needs-build-marker
Normal file
0
.pnpm-store/v11/.pnpm-needs-build-marker
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"dependencies":{"pnpm":"11.1.1"}}
|
||||||
BIN
.pnpm-store/v11/index.db
Normal file
BIN
.pnpm-store/v11/index.db
Normal file
Binary file not shown.
@@ -13,13 +13,8 @@
|
|||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
<<<<<<< HEAD
|
|
||||||
"seed:wagons": "ts-node -r tsconfig-paths/register src/scripts/seed-edr-wagons.ts",
|
"seed:wagons": "ts-node -r tsconfig-paths/register src/scripts/seed-edr-wagons.ts",
|
||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
=======
|
|
||||||
"type-check": "tsc --noEmit",
|
|
||||||
"seed:demo-scheduling": "ts-node -r tsconfig-paths/register src/scripts/seed-demo-scheduling.ts"
|
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edr/api-common": "workspace:*",
|
"@edr/api-common": "workspace:*",
|
||||||
|
|||||||
@@ -50,11 +50,8 @@ import { WagonsModule } from './modules/wagons/wagons.module';
|
|||||||
import { ContainersModule } from './modules/container-management/containers.module';
|
import { ContainersModule } from './modules/container-management/containers.module';
|
||||||
import { CargoesModule } from './modules/cargoes/cargoes.module';
|
import { CargoesModule } from './modules/cargoes/cargoes.module';
|
||||||
import { RoutesModule } from './modules/routes/routes.module';
|
import { RoutesModule } from './modules/routes/routes.module';
|
||||||
<<<<<<< HEAD
|
|
||||||
import { WarehousesModule } from './modules/warehouses/warehouses.module';
|
import { WarehousesModule } from './modules/warehouses/warehouses.module';
|
||||||
=======
|
|
||||||
import { OverviewModule } from './modules/overview/overview.module';
|
import { OverviewModule } from './modules/overview/overview.module';
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -109,11 +106,8 @@ import { OverviewModule } from './modules/overview/overview.module';
|
|||||||
ContainersModule,
|
ContainersModule,
|
||||||
CargoesModule,
|
CargoesModule,
|
||||||
RoutesModule,
|
RoutesModule,
|
||||||
<<<<<<< HEAD
|
|
||||||
WarehousesModule,
|
WarehousesModule,
|
||||||
=======
|
|
||||||
OverviewModule,
|
OverviewModule,
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
],
|
],
|
||||||
providers: [EdrOrgSeeder, DemoUsersSeeder,FreightStaffUsersSeeder, DemoBookingsSeeder, PricingDataSeeder, FileUploadSettingsSeeder],
|
providers: [EdrOrgSeeder, DemoUsersSeeder,FreightStaffUsersSeeder, DemoBookingsSeeder, PricingDataSeeder, FileUploadSettingsSeeder],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import * as Handlebars from "handlebars";
|
import * as Handlebars from "handlebars";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { SchedulingStatus } from "@edr/types";
|
|
||||||
import { Booking } from "../bookings/entities/booking.entity";
|
import { Booking } from "../bookings/entities/booking.entity";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -62,107 +61,6 @@ export class PaymentService {
|
|||||||
|
|
||||||
const result = await this.telebirrProvider.initiate(input);
|
const result = await this.telebirrProvider.initiate(input);
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
const queryRunner = this.datasource.createQueryRunner();
|
|
||||||
await queryRunner.connect();
|
|
||||||
await queryRunner.startTransaction();
|
|
||||||
|
|
||||||
console.log(paymentResp.expiresAt);
|
|
||||||
try {
|
|
||||||
const resp = await cb(queryRunner);
|
|
||||||
const payment = await this.paymentRepo.createTr(queryRunner, {
|
|
||||||
amount,
|
|
||||||
currency,
|
|
||||||
method,
|
|
||||||
refId: resp.id,
|
|
||||||
type: resp.type,
|
|
||||||
merchantOrderId: orderId,
|
|
||||||
rawInitiation: paymentResp.rawInitiation,
|
|
||||||
clientAction: paymentResp.clientAction,
|
|
||||||
expiresAt: paymentResp.expiresAt,
|
|
||||||
reason,
|
|
||||||
});
|
|
||||||
await queryRunner.commitTransaction();
|
|
||||||
return {
|
|
||||||
refId: payment.refId,
|
|
||||||
clientAction: paymentResp.clientAction,
|
|
||||||
status: payment.status,
|
|
||||||
paidAt: payment.paidAt?.toISOString(),
|
|
||||||
failureCode: payment.failerCode ?? undefined,
|
|
||||||
failureMessage: payment.failureMessage ?? undefined,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
await queryRunner.rollbackTransaction();
|
|
||||||
throw new Error("payment failed");
|
|
||||||
} finally {
|
|
||||||
await queryRunner.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getActivePaymentByRefIdAndMethod(
|
|
||||||
refId: string,
|
|
||||||
method: PaymentEntity["method"],
|
|
||||||
): Promise<PaymentEntity | null> {
|
|
||||||
return this.paymentRepo.getActivePaymentByRefIdAndMethod(refId, method);
|
|
||||||
}
|
|
||||||
|
|
||||||
async genReceiptHtml(orderId: string) {
|
|
||||||
const payment = await this.paymentRepo.findOneBy({
|
|
||||||
merchantOrderId: orderId,
|
|
||||||
status: "success",
|
|
||||||
});
|
|
||||||
if (!payment) {
|
|
||||||
throw new BadRequestException();
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = path.join(__dirname, "templates", "receipt.hbs");
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
throw new InternalServerErrorException();
|
|
||||||
}
|
|
||||||
const source = fs.readFileSync(filePath, "utf8");
|
|
||||||
const template = Handlebars.compile(source);
|
|
||||||
|
|
||||||
const html = template({
|
|
||||||
vendorName: "Ethio Djibouti Railway Ticket Booking",
|
|
||||||
vendorAddress: "Addis Ababa",
|
|
||||||
receiptDate: payment.paidAt,
|
|
||||||
paymentMethod: payment?.method,
|
|
||||||
subtotal: payment?.amount.toString(),
|
|
||||||
total: payment?.amount.toString(),
|
|
||||||
currency: payment?.currency,
|
|
||||||
reason: payment?.reason,
|
|
||||||
});
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkStatusAndUpdate(orderId: string) {
|
|
||||||
const resp = await this.paymentRepo.findOneBy({ merchantOrderId: orderId });
|
|
||||||
if (!resp) {
|
|
||||||
throw new NotFoundException("order id not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await this.telebirrPaymentStategy.queryStatus(
|
|
||||||
resp.merchantOrderId,
|
|
||||||
);
|
|
||||||
const bizContent = result.rawResponse.biz_content as {
|
|
||||||
order_status: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ordersStatus = bizContent.order_status;
|
|
||||||
if (ordersStatus == "PAY_SUCCESS") {
|
|
||||||
await this.datasource.transaction(async (mg) => {
|
|
||||||
const now = new Date();
|
|
||||||
const holdExpires = new Date(now.getTime() + 3 * 60 * 60 * 1000);
|
|
||||||
await mg.update(Booking, { id: resp.refId }, {
|
|
||||||
status: "PAID",
|
|
||||||
schedulingStatus: SchedulingStatus.Holding,
|
|
||||||
holdStartedAt: now,
|
|
||||||
holdExpiresAt: holdExpires,
|
|
||||||
});
|
|
||||||
await mg.update(PaymentEntity, { id: resp.id }, { status: "success" });
|
|
||||||
=======
|
|
||||||
const payment = await this.paymentRepo.create({
|
const payment = await this.paymentRepo.create({
|
||||||
amount: amount,
|
amount: amount,
|
||||||
currency: DEFAULT_CURRENCY,
|
currency: DEFAULT_CURRENCY,
|
||||||
@@ -174,7 +72,6 @@ export class PaymentService {
|
|||||||
clientAction: result.clientAction as Record<string, unknown>,
|
clientAction: result.clientAction as Record<string, unknown>,
|
||||||
expiresAt: result.expiresAt,
|
expiresAt: result.expiresAt,
|
||||||
reason: `Payment for booking`,
|
reason: `Payment for booking`,
|
||||||
>>>>>>> eda21e22d872344b74c0c72308f87ce7435b299f
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -8,22 +8,11 @@ import { TrainSet } from '../../train-sets/entities/train-set.entity';
|
|||||||
import { TrainScheduleBooking } from './train-schedule-booking.entity';
|
import { TrainScheduleBooking } from './train-schedule-booking.entity';
|
||||||
|
|
||||||
export const TRAIN_SCHEDULE_STATUSES = [
|
export const TRAIN_SCHEDULE_STATUSES = [
|
||||||
<<<<<<< HEAD
|
|
||||||
'DRAFT',
|
|
||||||
'READY',
|
|
||||||
'PUBLISHED',
|
|
||||||
'DEPARTED',
|
|
||||||
'IN_TRANSIT',
|
|
||||||
'ARRIVED',
|
|
||||||
'COMPLETED',
|
|
||||||
'CANCELLED',
|
|
||||||
=======
|
|
||||||
TrainScheduleStatusEnum.Draft,
|
TrainScheduleStatusEnum.Draft,
|
||||||
TrainScheduleStatusEnum.Scheduled,
|
TrainScheduleStatusEnum.Scheduled,
|
||||||
TrainScheduleStatusEnum.Dispatched,
|
TrainScheduleStatusEnum.Dispatched,
|
||||||
TrainScheduleStatusEnum.Arrived,
|
TrainScheduleStatusEnum.Arrived,
|
||||||
TrainScheduleStatusEnum.Cancelled,
|
TrainScheduleStatusEnum.Cancelled,
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type TrainScheduleStatus = (typeof TRAIN_SCHEDULE_STATUSES)[number];
|
export type TrainScheduleStatus = (typeof TRAIN_SCHEDULE_STATUSES)[number];
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
<<<<<<< HEAD
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { ArrayMinSize, IsArray, IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
|
|
||||||
=======
|
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsDateString, IsInt, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
|
import { IsDateString, IsInt, IsNumber, IsOptional, IsUUID, Min } from 'class-validator';
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
|
|
||||||
export class CreateContainerTrainScheduleDto {
|
export class CreateContainerTrainScheduleDto {
|
||||||
@ApiProperty({ format: 'uuid' })
|
@ApiProperty({ format: 'uuid' })
|
||||||
@@ -25,26 +20,6 @@ export class CreateContainerTrainScheduleDto {
|
|||||||
@IsUUID()
|
@IsUUID()
|
||||||
locomotiveId!: string;
|
locomotiveId!: string;
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
@ApiProperty({ enum: ['CONTAINER', 'BULK'], default: 'CONTAINER' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsIn(['CONTAINER', 'BULK'])
|
|
||||||
assignmentType?: 'CONTAINER' | 'BULK';
|
|
||||||
|
|
||||||
@ApiProperty({ type: [String] })
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@ArrayMinSize(1)
|
|
||||||
@IsUUID('4', { each: true })
|
|
||||||
bookingIds?: string[];
|
|
||||||
|
|
||||||
@ApiProperty({ type: [String], required: false })
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@ArrayMinSize(1)
|
|
||||||
@IsUUID('4', { each: true })
|
|
||||||
wagonIds?: string[];
|
|
||||||
=======
|
|
||||||
@ApiPropertyOptional({ description: 'Maximum total booking weight allowed on this train' })
|
@ApiPropertyOptional({ description: 'Maximum total booking weight allowed on this train' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@@ -65,5 +40,4 @@ export class CreateContainerTrainScheduleDto {
|
|||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
maxWagonsPerTrain?: number;
|
maxWagonsPerTrain?: number;
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
<<<<<<< HEAD
|
|
||||||
import { IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
|
|
||||||
=======
|
|
||||||
import { IsOptional, IsUUID } from 'class-validator';
|
import { IsOptional, IsUUID } from 'class-validator';
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
|
|
||||||
export class GetEligibleContainerBookingsDto {
|
export class GetEligibleContainerBookingsDto {
|
||||||
@ApiPropertyOptional({ format: 'uuid' })
|
@ApiPropertyOptional({ format: 'uuid' })
|
||||||
@@ -18,20 +14,5 @@ export class GetEligibleContainerBookingsDto {
|
|||||||
|
|
||||||
@ApiPropertyOptional({ example: 'HOLDING' })
|
@ApiPropertyOptional({ example: 'HOLDING' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
<<<<<<< HEAD
|
|
||||||
@IsDateString()
|
|
||||||
scheduleDate?: string;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ enum: ['CONTAINER', 'BULK'] })
|
|
||||||
@IsOptional()
|
|
||||||
@IsIn(['CONTAINER', 'BULK'])
|
|
||||||
assignmentType?: 'CONTAINER' | 'BULK';
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ enum: ['IMPORT', 'EXPORT', 'DOMESTIC'] })
|
|
||||||
@IsOptional()
|
|
||||||
@IsIn(['IMPORT', 'EXPORT', 'DOMESTIC'])
|
|
||||||
tradeDirection?: 'IMPORT' | 'EXPORT' | 'DOMESTIC';
|
|
||||||
=======
|
|
||||||
schedulingStatus?: string;
|
schedulingStatus?: string;
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,3 @@
|
|||||||
<<<<<<< HEAD
|
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
|
||||||
import { ArrayMinSize, IsArray, IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
|
|
||||||
|
|
||||||
export class PreviewContainerTrainScheduleDto {
|
|
||||||
@ApiProperty({ type: [String] })
|
|
||||||
@IsArray()
|
|
||||||
@ArrayMinSize(1)
|
|
||||||
@IsUUID('4', { each: true })
|
|
||||||
bookingIds!: string[];
|
|
||||||
|
|
||||||
@ApiProperty({ example: '2026-06-20T08:00:00.000Z' })
|
|
||||||
@IsDateString()
|
|
||||||
scheduleDate!: string;
|
|
||||||
|
|
||||||
@ApiProperty({ format: 'uuid' })
|
|
||||||
@IsUUID()
|
|
||||||
originStationId!: string;
|
|
||||||
|
|
||||||
@ApiProperty({ format: 'uuid' })
|
|
||||||
@IsUUID()
|
|
||||||
destinationStationId!: string;
|
|
||||||
|
|
||||||
@ApiProperty({ enum: ['CONTAINER', 'BULK'], default: 'CONTAINER' })
|
|
||||||
@IsOptional()
|
|
||||||
@IsIn(['CONTAINER', 'BULK'])
|
|
||||||
assignmentType?: 'CONTAINER' | 'BULK';
|
|
||||||
}
|
|
||||||
=======
|
|
||||||
import { PreviewTrainScheduleDto } from './preview-train-schedule.dto';
|
import { PreviewTrainScheduleDto } from './preview-train-schedule.dto';
|
||||||
|
|
||||||
export class PreviewContainerTrainScheduleDto extends PreviewTrainScheduleDto {}
|
export class PreviewContainerTrainScheduleDto extends PreviewTrainScheduleDto {}
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
|
|||||||
@@ -196,17 +196,10 @@ export class TrainSchedulingController {
|
|||||||
return this.trainSchedulingService.cancelTrainSchedule(id);
|
return this.trainSchedulingService.cancelTrainSchedule(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
@Post('container/schedules/:id/publish')
|
|
||||||
@ApiOperation({ summary: 'Publish container train schedule' })
|
|
||||||
publishTrainSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
|
||||||
return this.trainSchedulingService.publishTrainSchedule(id);
|
|
||||||
=======
|
|
||||||
@Post('bulk/schedules/:id/cancel')
|
@Post('bulk/schedules/:id/cancel')
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: 'Cancel bulk train schedule' })
|
@ApiOperation({ summary: 'Cancel bulk train schedule' })
|
||||||
cancelBulkTrainSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
cancelBulkTrainSchedule(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.trainSchedulingService.cancelTrainSchedule(id);
|
return this.trainSchedulingService.cancelTrainSchedule(id);
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,18 +14,7 @@ import { TrainSchedulesModule } from '../train-schedules/train-schedules.module'
|
|||||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||||
import { WagonTypesModule } from '../wagon-types/wagon-types.module';
|
import { WagonTypesModule } from '../wagon-types/wagon-types.module';
|
||||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||||
<<<<<<< HEAD
|
|
||||||
import { TrainSet } from '../train-sets/entities/train-set.entity';
|
|
||||||
import { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity';
|
|
||||||
import { TrainSetsModule } from '../train-sets/train-sets.module';
|
|
||||||
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 { TrainSchedulesModule } from '../train-schedules/train-schedules.module';
|
|
||||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
|
||||||
=======
|
|
||||||
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
|
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
import { TrainSchedulingController } from './train-scheduling.controller';
|
import { TrainSchedulingController } from './train-scheduling.controller';
|
||||||
import { TrainSchedulingService } from './train-scheduling.service';
|
import { TrainSchedulingService } from './train-scheduling.service';
|
||||||
|
|
||||||
|
|||||||
@@ -58,15 +58,10 @@ const makeBooking = (
|
|||||||
scheduledDate: new Date(scheduledDate),
|
scheduledDate: new Date(scheduledDate),
|
||||||
originYardId,
|
originYardId,
|
||||||
destinationYardId,
|
destinationYardId,
|
||||||
<<<<<<< HEAD
|
|
||||||
status: 'APPROVED',
|
|
||||||
customer: { companyName: 'Demo Customer' },
|
|
||||||
=======
|
|
||||||
status: 'PAID',
|
status: 'PAID',
|
||||||
schedulingStatus: 'HOLDING',
|
schedulingStatus: 'HOLDING',
|
||||||
holdExpiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
holdExpiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||||
company: { companyName: 'Demo Customer' },
|
company: { companyName: 'Demo Customer' },
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
originYard: { label: 'Djibouti', code: 'DJIBOUTI' },
|
originYard: { label: 'Djibouti', code: 'DJIBOUTI' },
|
||||||
destinationYard: { label: 'Addis Ababa', code: 'ADDIS_ABABA' },
|
destinationYard: { label: 'Addis Ababa', code: 'ADDIS_ABABA' },
|
||||||
bookingContainers: [
|
bookingContainers: [
|
||||||
@@ -352,14 +347,7 @@ describe('TrainSchedulingService', () => {
|
|||||||
|
|
||||||
it('rejects bookings that are not in assignable status', async () => {
|
it('rejects bookings that are not in assignable status', async () => {
|
||||||
const bookings = [
|
const bookings = [
|
||||||
<<<<<<< HEAD
|
|
||||||
{
|
|
||||||
...makeBooking('b7', 'BKG-CONT-007', 120, 2, '40FT'),
|
|
||||||
status: 'PAID',
|
|
||||||
},
|
|
||||||
=======
|
|
||||||
{ ...makeBooking('b7', 'BKG-CONT-007', 120, 2, '40FT', 2), status: 'APPROVED' },
|
{ ...makeBooking('b7', 'BKG-CONT-007', 120, 2, '40FT', 2), status: 'APPROVED' },
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
];
|
];
|
||||||
|
|
||||||
wagonTypesRepository.findAll.mockResolvedValue([nw5]);
|
wagonTypesRepository.findAll.mockResolvedValue([nw5]);
|
||||||
|
|||||||
@@ -46,73 +46,6 @@ import { PreviewTrainScheduleDto } from './dto/preview-train-schedule.dto';
|
|||||||
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
|
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
|
||||||
import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduling-global-rules.dto';
|
import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduling-global-rules.dto';
|
||||||
import {
|
import {
|
||||||
<<<<<<< HEAD
|
|
||||||
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 { Route } from "../routes/entities/route.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 { Wagon } from "../wagons/entities/wagon.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 MAX_TRAIN_WEIGHT_TONS = 3500;
|
|
||||||
const MAX_TRAIN_LENGTH_METERS = 760;
|
|
||||||
const ASSIGNABLE_BOOKING_STATUSES = ["APPROVED", "READY_FOR_ASSIGNMENT"] as const;
|
|
||||||
const EXCLUDED_BOOKING_STATUSES = ["CANCELLED", "COMPLETED", "IN_TRANSIT", "ARRIVED"] as const;
|
|
||||||
const MAX_CONTAINER_WAGONS = 53;
|
|
||||||
const MAX_BULK_WAGONS = 37;
|
|
||||||
|
|
||||||
type EligibleBookingItem = {
|
|
||||||
id: string;
|
|
||||||
reference: string;
|
|
||||||
customer: string;
|
|
||||||
containerType: string;
|
|
||||||
quantity: number;
|
|
||||||
weightTons: number;
|
|
||||||
origin: string;
|
|
||||||
destination: string;
|
|
||||||
preferredDepartureDate: string;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type WagonAllocationRecord = {
|
|
||||||
bookingId: string;
|
|
||||||
bookingReference: string;
|
|
||||||
allocatedWeightTons: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type WagonPlanRecord = {
|
|
||||||
sequenceNo: number;
|
|
||||||
capacityTons: number;
|
|
||||||
lengthMeters: number;
|
|
||||||
assignedWeightTons: number;
|
|
||||||
allocations: WagonAllocationRecord[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ValidationResult = {
|
|
||||||
valid: boolean;
|
|
||||||
violations: string[];
|
|
||||||
bookings: Booking[];
|
|
||||||
wagonType: WagonType;
|
|
||||||
summary: {
|
|
||||||
totalBookings: number;
|
|
||||||
totalWeightTons: number;
|
|
||||||
wagonType: string;
|
|
||||||
wagonsNeeded: number;
|
|
||||||
totalLengthMeters: number;
|
|
||||||
};
|
|
||||||
wagonPlan: WagonPlanRecord[];
|
|
||||||
=======
|
|
||||||
buildCappedWagonPlan,
|
buildCappedWagonPlan,
|
||||||
computeFleetAvailability,
|
computeFleetAvailability,
|
||||||
selectBookingsWithinFleetCap,
|
selectBookingsWithinFleetCap,
|
||||||
@@ -150,7 +83,6 @@ const DEFAULT_TRAIN_LIMITS: Required<TrainLimitConfig> = {
|
|||||||
maxWagonsPerTrain: 53,
|
maxWagonsPerTrain: 53,
|
||||||
max20ftContainerWeightTons: 30,
|
max20ftContainerWeightTons: 30,
|
||||||
max20ftPairWeightDiffTons: 10,
|
max20ftPairWeightDiffTons: 10,
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -180,36 +112,12 @@ export class TrainSchedulingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getEligibleContainerBookings(query: GetEligibleContainerBookingsDto) {
|
async getEligibleContainerBookings(query: GetEligibleContainerBookingsDto) {
|
||||||
<<<<<<< HEAD
|
|
||||||
const bookingRepository = this.dataSource.getRepository(Booking);
|
|
||||||
const queryBuilder = bookingRepository
|
|
||||||
.createQueryBuilder("booking")
|
|
||||||
.leftJoinAndSelect("booking.company", "company")
|
|
||||||
.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: query.assignmentType ?? "CONTAINER",
|
|
||||||
})
|
|
||||||
.andWhere("scheduleBooking.id IS NULL");
|
|
||||||
|
|
||||||
queryBuilder.andWhere("booking.status IN (:...assignableStatuses)", {
|
|
||||||
assignableStatuses: ASSIGNABLE_BOOKING_STATUSES,
|
|
||||||
});
|
|
||||||
=======
|
|
||||||
return this.getEligibleBookings({ ...query, freightType: 'CONTAINER' });
|
return this.getEligibleBookings({ ...query, freightType: 'CONTAINER' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEligibleBulkBookings(query: GetEligibleBulkBookingsDto) {
|
async getEligibleBulkBookings(query: GetEligibleBulkBookingsDto) {
|
||||||
return this.getEligibleBookings({ ...query, freightType: 'BULK' });
|
return this.getEligibleBookings({ ...query, freightType: 'BULK' });
|
||||||
}
|
}
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
|
|
||||||
async getTrainSchedulingGlobalRules() {
|
async getTrainSchedulingGlobalRules() {
|
||||||
return this.loadGlobalRulesRow();
|
return this.loadGlobalRulesRow();
|
||||||
@@ -226,84 +134,12 @@ export class TrainSchedulingService {
|
|||||||
if (dto.max20ftContainerWeightTons != null) {
|
if (dto.max20ftContainerWeightTons != null) {
|
||||||
row.max20ftContainerWeightTons = dto.max20ftContainerWeightTons;
|
row.max20ftContainerWeightTons = dto.max20ftContainerWeightTons;
|
||||||
}
|
}
|
||||||
<<<<<<< HEAD
|
|
||||||
|
|
||||||
if (query.tradeDirection === "IMPORT") {
|
|
||||||
queryBuilder.andWhere(
|
|
||||||
`(
|
|
||||||
lower(originYard.country) IN ('djibouti', 'djoubti', 'dj')
|
|
||||||
OR lower(originYard.code) LIKE '%djib%'
|
|
||||||
OR lower(originYard.label) LIKE '%djib%'
|
|
||||||
)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.tradeDirection === "EXPORT") {
|
|
||||||
queryBuilder.andWhere(
|
|
||||||
`(
|
|
||||||
lower(destinationYard.country) IN ('djibouti', 'djoubti', 'dj')
|
|
||||||
OR lower(destinationYard.code) LIKE '%djib%'
|
|
||||||
OR lower(destinationYard.label) LIKE '%djib%'
|
|
||||||
)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.scheduleDate) {
|
|
||||||
queryBuilder.andWhere(
|
|
||||||
`DATE(booking.scheduled_date AT TIME ZONE 'UTC') = :scheduleDate`,
|
|
||||||
{ scheduleDate: this.toUtcDateKey(query.scheduleDate) },
|
|
||||||
);
|
|
||||||
=======
|
|
||||||
if (dto.max20ftPairWeightDiffTons != null) {
|
if (dto.max20ftPairWeightDiffTons != null) {
|
||||||
row.max20ftPairWeightDiffTons = dto.max20ftPairWeightDiffTons;
|
row.max20ftPairWeightDiffTons = dto.max20ftPairWeightDiffTons;
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
return this.dataSource.getRepository(TrainSchedulingGlobalRules).save(row);
|
return this.dataSource.getRepository(TrainSchedulingGlobalRules).save(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
const bookings = await queryBuilder
|
|
||||||
.orderBy("booking.scheduled_date", "ASC")
|
|
||||||
.addOrderBy("booking.created_at", "ASC")
|
|
||||||
.getMany();
|
|
||||||
|
|
||||||
const items: EligibleBookingItem[] = bookings.map((booking) => ({
|
|
||||||
id: booking.id,
|
|
||||||
reference: booking.reference,
|
|
||||||
customer:
|
|
||||||
booking.company?.name ?? booking.company?.email ?? "Unknown customer",
|
|
||||||
containerType:
|
|
||||||
booking.bookingContainers
|
|
||||||
?.map(
|
|
||||||
(container) =>
|
|
||||||
container.containerType?.label ??
|
|
||||||
container.containerType?.code ??
|
|
||||||
"Container",
|
|
||||||
)
|
|
||||||
.join(", ") ?? (booking.freightType === "BULK" ? "Bulk cargo" : "Container"),
|
|
||||||
quantity:
|
|
||||||
booking.bookingContainers?.reduce(
|
|
||||||
(sum, container) => sum + Number(container.quantity ?? 0),
|
|
||||||
0,
|
|
||||||
) ?? 0,
|
|
||||||
weightTons: this.roundTons(booking.cargoTotalWeightVgm),
|
|
||||||
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,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
count: items.length,
|
|
||||||
items,
|
|
||||||
};
|
|
||||||
=======
|
|
||||||
async previewTrainSchedule(dto: PreviewTrainScheduleDto) {
|
async previewTrainSchedule(dto: PreviewTrainScheduleDto) {
|
||||||
const limits = await this.resolveTrainLimitConfig(dto);
|
const limits = await this.resolveTrainLimitConfig(dto);
|
||||||
return this.buildPreviewResponse(
|
return this.buildPreviewResponse(
|
||||||
@@ -317,7 +153,6 @@ export class TrainSchedulingService {
|
|||||||
dto.targetScheduleId,
|
dto.targetScheduleId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async previewContainerTrainSchedule(dto: PreviewContainerTrainScheduleDto) {
|
async previewContainerTrainSchedule(dto: PreviewContainerTrainScheduleDto) {
|
||||||
@@ -370,29 +205,6 @@ export class TrainSchedulingService {
|
|||||||
|
|
||||||
async createContainerTrainSchedule(dto: CreateContainerTrainScheduleDto) {
|
async createContainerTrainSchedule(dto: CreateContainerTrainScheduleDto) {
|
||||||
const route = await this.getActiveRoute(dto.routeId);
|
const route = await this.getActiveRoute(dto.routeId);
|
||||||
<<<<<<< HEAD
|
|
||||||
const validation = dto.bookingIds?.length
|
|
||||||
? await this.validateContainerBookingsForScheduling({
|
|
||||||
bookingIds: dto.bookingIds,
|
|
||||||
scheduleDate: dto.scheduleDate,
|
|
||||||
originStationId: route.originYardId,
|
|
||||||
destinationStationId: route.destinationYardId,
|
|
||||||
assignmentType: dto.assignmentType ?? "CONTAINER",
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (validation && !validation.valid) {
|
|
||||||
throw new BadRequestException({
|
|
||||||
message: "Train schedule assignment is invalid",
|
|
||||||
violations: validation.violations,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const locomotive = await this.selectOrValidateLocomotive(
|
|
||||||
dto.locomotiveId,
|
|
||||||
validation?.summary.totalWeightTons ?? 0,
|
|
||||||
validation?.summary.totalLengthMeters ?? 0,
|
|
||||||
=======
|
|
||||||
const locomotive = await this.selectOrValidateLocomotive(dto.locomotiveId, 0, 0);
|
const locomotive = await this.selectOrValidateLocomotive(dto.locomotiveId, 0, 0);
|
||||||
|
|
||||||
const createdScheduleId = await this.dataSource.transaction(async (manager) => {
|
const createdScheduleId = await this.dataSource.transaction(async (manager) => {
|
||||||
@@ -467,7 +279,6 @@ export class TrainSchedulingService {
|
|||||||
true,
|
true,
|
||||||
limits,
|
limits,
|
||||||
scheduleId,
|
scheduleId,
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
@@ -671,42 +482,8 @@ export class TrainSchedulingService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
const selectedPhysicalWagons = validation
|
|
||||||
? await this.lockSelectedWagonsForSchedule(
|
|
||||||
manager,
|
|
||||||
dto.wagonIds ?? [],
|
|
||||||
validation.wagonPlan.length,
|
|
||||||
route,
|
|
||||||
dto.assignmentType ?? "CONTAINER",
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const trainSetResult = validation
|
|
||||||
? await this.buildTrainSet(
|
|
||||||
manager,
|
|
||||||
lockedLocomotive,
|
|
||||||
validation.wagonType,
|
|
||||||
validation.summary.totalWeightTons,
|
|
||||||
validation.summary.totalLengthMeters,
|
|
||||||
validation.wagonPlan,
|
|
||||||
selectedPhysicalWagons,
|
|
||||||
)
|
|
||||||
: { trainSet: await this.buildEmptyTrainSet(manager, lockedLocomotive), wagons: [] };
|
|
||||||
const { trainSet, wagons } = trainSetResult;
|
|
||||||
|
|
||||||
const schedule = manager.getRepository(TrainSchedule).create({
|
|
||||||
trainSetId: trainSet.id,
|
|
||||||
routeId: route.id,
|
|
||||||
originStationId: route.originYardId,
|
|
||||||
destinationStationId: route.destinationYardId,
|
|
||||||
scheduledDepartureDate: new Date(dto.scheduleDate),
|
|
||||||
scheduledArrivalDate: dto.arrivalDate ? new Date(dto.arrivalDate) : null,
|
|
||||||
status: validation ? "READY" : "DRAFT",
|
|
||||||
=======
|
|
||||||
const physicalWagon = await manager.getRepository(Wagon).findOne({
|
const physicalWagon = await manager.getRepository(Wagon).findOne({
|
||||||
where: { id: assignment.physicalWagonId },
|
where: { id: assignment.physicalWagonId },
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
});
|
});
|
||||||
if (!physicalWagon) {
|
if (!physicalWagon) {
|
||||||
throw new NotFoundException(`Wagon ${assignment.physicalWagonId} not found`);
|
throw new NotFoundException(`Wagon ${assignment.physicalWagonId} not found`);
|
||||||
@@ -725,55 +502,9 @@ export class TrainSchedulingService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
const savedSchedule = await manager
|
|
||||||
.getRepository(TrainSchedule)
|
|
||||||
.save(schedule);
|
|
||||||
|
|
||||||
if (validation) {
|
|
||||||
await manager.getRepository(TrainScheduleBooking).save(
|
|
||||||
validation.bookings.map((booking) =>
|
|
||||||
manager.getRepository(TrainScheduleBooking).create({
|
|
||||||
trainScheduleId: savedSchedule.id,
|
|
||||||
bookingId: booking.id,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const wagonBySequence = new Map(wagons.map((wagon) => [wagon.sequenceNo, wagon]));
|
|
||||||
const allocations = validation.wagonPlan.flatMap((wagonPlan) => {
|
|
||||||
const savedWagon = wagonBySequence.get(wagonPlan.sequenceNo);
|
|
||||||
if (!savedWagon) return [];
|
|
||||||
|
|
||||||
return wagonPlan.allocations.map((allocation) =>
|
|
||||||
manager.getRepository(WagonBookingAllocation).create({
|
|
||||||
trainSetWagonId: savedWagon.id,
|
|
||||||
bookingId: allocation.bookingId,
|
|
||||||
allocatedWeightTons: allocation.allocatedWeightTons,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (allocations.length > 0) {
|
|
||||||
await manager.getRepository(WagonBookingAllocation).save(allocations);
|
|
||||||
}
|
|
||||||
|
|
||||||
await manager.getRepository(Booking).update(
|
|
||||||
{ id: In(validation.bookings.map((booking) => booking.id)) },
|
|
||||||
{
|
|
||||||
status: "INVOICED",
|
|
||||||
paymentStatus: "PENDING",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await locomotiveRepository.update(lockedLocomotive.id, {
|
|
||||||
status: "ASSIGNED",
|
|
||||||
=======
|
|
||||||
await manager.getRepository(TrainSetWagon).update(assignment.trainSetWagonId, {
|
await manager.getRepository(TrainSetWagon).update(assignment.trainSetWagonId, {
|
||||||
physicalWagonId: assignment.physicalWagonId,
|
physicalWagonId: assignment.physicalWagonId,
|
||||||
status: 'RESERVED',
|
status: 'RESERVED',
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
});
|
});
|
||||||
await manager.getRepository(Wagon).update(assignment.physicalWagonId, {
|
await manager.getRepository(Wagon).update(assignment.physicalWagonId, {
|
||||||
trainSetWagonId: assignment.trainSetWagonId,
|
trainSetWagonId: assignment.trainSetWagonId,
|
||||||
@@ -950,10 +681,6 @@ export class TrainSchedulingService {
|
|||||||
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');
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
const nonContainerBookings = bookings.filter(
|
|
||||||
(booking) => booking.freightType !== (dto.assignmentType ?? "CONTAINER"),
|
|
||||||
=======
|
|
||||||
const bookingTypes = new Set(bookings.map((b) => b.freightType));
|
const bookingTypes = new Set(bookings.map((b) => b.freightType));
|
||||||
const isMixed = bookingTypes.size > 1;
|
const isMixed = bookingTypes.size > 1;
|
||||||
const resolvedMode: 'CONTAINER' | 'BULK' | 'MIXED' =
|
const resolvedMode: 'CONTAINER' | 'BULK' | 'MIXED' =
|
||||||
@@ -968,117 +695,15 @@ export class TrainSchedulingService {
|
|||||||
|
|
||||||
const invalidStatus = bookings.filter(
|
const invalidStatus = bookings.filter(
|
||||||
(b) => !SCHEDULABLE_BOOKING_STATUSES.includes(b.status as 'PAID'),
|
(b) => !SCHEDULABLE_BOOKING_STATUSES.includes(b.status as 'PAID'),
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
);
|
);
|
||||||
if (invalidStatus.length) {
|
if (invalidStatus.length) {
|
||||||
const statuses = [...new Set(invalidStatus.map((b) => b.status))];
|
const statuses = [...new Set(invalidStatus.map((b) => b.status))];
|
||||||
violations.push(
|
violations.push(
|
||||||
<<<<<<< HEAD
|
|
||||||
`Only ${dto.assignmentType ?? "CONTAINER"} bookings are supported for this assignment`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const invalidStatusBookings = bookings.filter(
|
|
||||||
(booking) =>
|
|
||||||
!ASSIGNABLE_BOOKING_STATUSES.includes(booking.status as (typeof ASSIGNABLE_BOOKING_STATUSES)[number]),
|
|
||||||
);
|
|
||||||
if (invalidStatusBookings.length > 0) {
|
|
||||||
const invalidStatuses = [...new Set(invalidStatusBookings.map((booking) => booking.status))];
|
|
||||||
violations.push(
|
|
||||||
`Only ${ASSIGNABLE_BOOKING_STATUSES.join(", ")} bookings can be assigned; received: ${invalidStatuses.join(", ")}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const excludedStatusBookings = bookings.filter((booking) =>
|
|
||||||
EXCLUDED_BOOKING_STATUSES.includes(booking.status as (typeof EXCLUDED_BOOKING_STATUSES)[number]),
|
|
||||||
);
|
|
||||||
if (excludedStatusBookings.length > 0) {
|
|
||||||
violations.push(`Cancelled, completed, in-transit, or arrived bookings cannot be assigned`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduleDateKey = this.toUtcDateKey(dto.scheduleDate);
|
|
||||||
const routeMismatch = bookings.some(
|
|
||||||
(booking) =>
|
|
||||||
booking.originYardId !== dto.originStationId ||
|
|
||||||
booking.destinationYardId !== dto.destinationStationId,
|
|
||||||
);
|
|
||||||
if (routeMismatch) {
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueOriginCount = new Set(
|
|
||||||
bookings.map((booking) => booking.originYardId),
|
|
||||||
).size;
|
|
||||||
if (uniqueOriginCount > 1) {
|
|
||||||
violations.push("Selected bookings must share the same origin station");
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueDestinationCount = new Set(
|
|
||||||
bookings.map((booking) => booking.destinationYardId),
|
|
||||||
).size;
|
|
||||||
if (uniqueDestinationCount > 1) {
|
|
||||||
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",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalWeightTons = this.roundTons(
|
|
||||||
bookings.reduce(
|
|
||||||
(sum, booking) => sum + Number(booking.cargoTotalWeightVgm ?? 0),
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
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`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalLengthMeters > MAX_TRAIN_LENGTH_METERS) {
|
|
||||||
violations.push(
|
|
||||||
`Total wagon length ${totalLengthMeters}m exceeds max train length ${MAX_TRAIN_LENGTH_METERS}m`,
|
|
||||||
=======
|
|
||||||
`Only ${SCHEDULABLE_BOOKING_STATUSES.join(', ')} bookings can be scheduled; received: ${statuses.join(', ')}`,
|
`Only ${SCHEDULABLE_BOOKING_STATUSES.join(', ')} bookings can be scheduled; received: ${statuses.join(', ')}`,
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
<<<<<<< HEAD
|
|
||||||
wagonPlan.length >
|
|
||||||
(dto.assignmentType === "BULK" ? MAX_BULK_WAGONS : MAX_CONTAINER_WAGONS)
|
|
||||||
) {
|
|
||||||
violations.push(
|
|
||||||
`Wagon count ${wagonPlan.length} exceeds ${dto.assignmentType === "BULK" ? "bulk" : "container"} limit ${dto.assignmentType === "BULK" ? MAX_BULK_WAGONS : MAX_CONTAINER_WAGONS}`,
|
|
||||||
);
|
|
||||||
=======
|
|
||||||
bookings.some(
|
bookings.some(
|
||||||
(b) =>
|
(b) =>
|
||||||
b.originYardId !== dto.originStationId ||
|
b.originYardId !== dto.originStationId ||
|
||||||
@@ -1086,7 +711,6 @@ export class TrainSchedulingService {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
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');
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!forceAssign) {
|
if (!forceAssign) {
|
||||||
@@ -1644,139 +1268,7 @@ export class TrainSchedulingService {
|
|||||||
return locomotive;
|
return locomotive;
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
async lockSelectedWagonsForSchedule(
|
|
||||||
manager: EntityManager,
|
|
||||||
wagonIds: string[],
|
|
||||||
requiredCount: number,
|
|
||||||
route: Route,
|
|
||||||
assignmentType: "CONTAINER" | "BULK",
|
|
||||||
) {
|
|
||||||
const uniqueWagonIds = [...new Set(wagonIds)];
|
|
||||||
|
|
||||||
if (uniqueWagonIds.length < requiredCount) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
`Select at least ${requiredCount} available wagons for this schedule`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const wagons = await manager
|
|
||||||
.getRepository(Wagon)
|
|
||||||
.createQueryBuilder("wagon")
|
|
||||||
.leftJoinAndSelect("wagon.wagonType", "wagonType")
|
|
||||||
.where("wagon.id IN (:...wagonIds)", { wagonIds: uniqueWagonIds })
|
|
||||||
.setLock("pessimistic_write")
|
|
||||||
.getMany();
|
|
||||||
|
|
||||||
if (wagons.length !== uniqueWagonIds.length) {
|
|
||||||
throw new BadRequestException("One or more selected wagons were not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedStatus = this.expectedWagonStatusForRoute(route);
|
|
||||||
const allowedStatuses = new Set([
|
|
||||||
expectedStatus,
|
|
||||||
"AVAILABLE",
|
|
||||||
...(expectedStatus === "EXPORT_READY" ? ["IMPORT_READY"] : []),
|
|
||||||
]);
|
|
||||||
const invalidWagon = wagons.find(
|
|
||||||
(wagon) =>
|
|
||||||
wagon.trainId ||
|
|
||||||
wagon.status === "ASSIGNED" ||
|
|
||||||
wagon.currentLocationYardId !== route.originYardId ||
|
|
||||||
!allowedStatuses.has(wagon.status) ||
|
|
||||||
!this.wagonTypeSupportsAssignment(wagon, assignmentType),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (invalidWagon) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
`Wagon ${invalidWagon.wagonNumber} is not at the route origin or is not ready for this ${this.routeDirection(route).toLowerCase()} route`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const wagonById = new Map(wagons.map((wagon) => [wagon.id, wagon]));
|
|
||||||
return uniqueWagonIds.slice(0, requiredCount).map((wagonId) => wagonById.get(wagonId)!);
|
|
||||||
}
|
|
||||||
|
|
||||||
private wagonTypeSupportsAssignment(wagon: Wagon, assignmentType: "CONTAINER" | "BULK") {
|
|
||||||
const supportedLoadTypes = wagon.wagonType?.supportedLoadTypes ?? [];
|
|
||||||
const normalized = supportedLoadTypes.map((loadType) => loadType.trim().toUpperCase());
|
|
||||||
return normalized.includes(assignmentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private routeDirection(route: Route) {
|
|
||||||
const originCountry = route.originYard?.country?.trim().toLowerCase();
|
|
||||||
const destinationCountry = route.destinationYard?.country?.trim().toLowerCase();
|
|
||||||
const isOriginEthiopia = originCountry === "ethiopia" || originCountry === "et";
|
|
||||||
const isDestinationEthiopia = destinationCountry === "ethiopia" || destinationCountry === "et";
|
|
||||||
|
|
||||||
if (!isOriginEthiopia && isDestinationEthiopia) return "IMPORT";
|
|
||||||
if (isOriginEthiopia && !isDestinationEthiopia) return "EXPORT";
|
|
||||||
return "DOMESTIC";
|
|
||||||
}
|
|
||||||
|
|
||||||
private expectedWagonStatusForRoute(route: Route) {
|
|
||||||
const direction = this.routeDirection(route);
|
|
||||||
if (direction === "IMPORT") return "IMPORT_READY";
|
|
||||||
if (direction === "EXPORT" || direction === "DOMESTIC") return "EXPORT_READY";
|
|
||||||
return "AVAILABLE";
|
|
||||||
}
|
|
||||||
|
|
||||||
async buildTrainSet(
|
|
||||||
manager: EntityManager,
|
|
||||||
locomotive: Locomotive,
|
|
||||||
wagonType: WagonType,
|
|
||||||
totalWeightTons: number,
|
|
||||||
totalLengthMeters: number,
|
|
||||||
wagonPlan: WagonPlanRecord[],
|
|
||||||
physicalWagons: Wagon[] = [],
|
|
||||||
) {
|
|
||||||
const trainSet = manager.getRepository(TrainSet).create({
|
|
||||||
locomotiveId: locomotive.id,
|
|
||||||
totalWeightTons,
|
|
||||||
totalLengthMeters,
|
|
||||||
wagonCount: wagonPlan.length,
|
|
||||||
status: "ASSIGNED",
|
|
||||||
});
|
|
||||||
const savedTrainSet = await manager.getRepository(TrainSet).save(trainSet);
|
|
||||||
|
|
||||||
const wagons = wagonPlan.map((wagon, index) => {
|
|
||||||
const physicalWagon = physicalWagons[index];
|
|
||||||
const selectedWagonType = physicalWagon?.wagonType ?? wagonType;
|
|
||||||
|
|
||||||
return manager.getRepository(TrainSetWagon).create({
|
|
||||||
trainSetId: savedTrainSet.id,
|
|
||||||
wagonTypeId: selectedWagonType.id,
|
|
||||||
physicalWagonId: physicalWagon?.id ?? null,
|
|
||||||
sequenceNo: wagon.sequenceNo,
|
|
||||||
capacityTons: Number(selectedWagonType.capacityTons),
|
|
||||||
lengthMeters: Number(selectedWagonType.lengthMeters),
|
|
||||||
assignedWeightTons: wagon.assignedWeightTons,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const savedWagons = await manager.getRepository(TrainSetWagon).save(wagons);
|
|
||||||
|
|
||||||
if (physicalWagons.length > 0) {
|
|
||||||
await Promise.all(
|
|
||||||
physicalWagons.map((wagon, index) =>
|
|
||||||
manager.getRepository(Wagon).update(wagon.id, {
|
|
||||||
status: "ASSIGNED",
|
|
||||||
sequenceNumber: index + 1,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { trainSet: savedTrainSet, wagons: savedWagons };
|
|
||||||
}
|
|
||||||
|
|
||||||
async buildEmptyTrainSet(
|
|
||||||
manager: EntityManager,
|
|
||||||
locomotive: Locomotive,
|
|
||||||
) {
|
|
||||||
=======
|
|
||||||
private async buildEmptyTrainSet(manager: EntityManager, locomotive: Locomotive) {
|
private async buildEmptyTrainSet(manager: EntityManager, locomotive: Locomotive) {
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
const trainSet = manager.getRepository(TrainSet).create({
|
const trainSet = manager.getRepository(TrainSet).create({
|
||||||
locomotiveId: locomotive.id,
|
locomotiveId: locomotive.id,
|
||||||
totalWeightTons: 0,
|
totalWeightTons: 0,
|
||||||
@@ -1787,313 +1279,6 @@ export class TrainSchedulingService {
|
|||||||
return manager.getRepository(TrainSet).save(trainSet);
|
return manager.getRepository(TrainSet).save(trainSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
allocateBookingsToWagons(
|
|
||||||
bookings: Booking[],
|
|
||||||
baseWagonPlan: WagonPlanRecord[],
|
|
||||||
): WagonPlanRecord[] {
|
|
||||||
const remaining = bookings.map((booking) => ({
|
|
||||||
bookingId: booking.id,
|
|
||||||
bookingReference: booking.reference,
|
|
||||||
remainingWeightTons: this.roundTons(
|
|
||||||
Number(booking.cargoTotalWeightVgm ?? 0),
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
let bookingIndex = 0;
|
|
||||||
|
|
||||||
return baseWagonPlan.map((wagon) => {
|
|
||||||
let wagonRemaining = this.roundTons(wagon.capacityTons);
|
|
||||||
const allocations: WagonAllocationRecord[] = [];
|
|
||||||
let assignedWeightTons = 0;
|
|
||||||
|
|
||||||
while (wagonRemaining > 0 && bookingIndex < remaining.length) {
|
|
||||||
const booking = remaining[bookingIndex];
|
|
||||||
const allocatedWeightTons = this.roundTons(
|
|
||||||
Math.min(wagonRemaining, booking.remainingWeightTons),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (allocatedWeightTons <= 0) {
|
|
||||||
bookingIndex += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
allocations.push({
|
|
||||||
bookingId: booking.bookingId,
|
|
||||||
bookingReference: booking.bookingReference,
|
|
||||||
allocatedWeightTons,
|
|
||||||
});
|
|
||||||
booking.remainingWeightTons = this.roundTons(
|
|
||||||
booking.remainingWeightTons - allocatedWeightTons,
|
|
||||||
);
|
|
||||||
wagonRemaining = this.roundTons(wagonRemaining - allocatedWeightTons);
|
|
||||||
assignedWeightTons = this.roundTons(
|
|
||||||
assignedWeightTons + allocatedWeightTons,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (booking.remainingWeightTons <= 0) {
|
|
||||||
bookingIndex += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...wagon,
|
|
||||||
assignedWeightTons,
|
|
||||||
allocations,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getContainerTrainSchedules() {
|
|
||||||
const schedules = await this.dataSource.getRepository(TrainSchedule).find({
|
|
||||||
relations: {
|
|
||||||
trainSet: { locomotive: true },
|
|
||||||
route: true,
|
|
||||||
originStation: true,
|
|
||||||
destinationStation: true,
|
|
||||||
scheduleBookings: true,
|
|
||||||
},
|
|
||||||
order: { scheduledDepartureDate: "DESC", createdAt: "DESC" },
|
|
||||||
});
|
|
||||||
|
|
||||||
return schedules.map((schedule) => ({
|
|
||||||
id: schedule.id,
|
|
||||||
scheduleDate: schedule.scheduledDepartureDate,
|
|
||||||
routeName: schedule.route?.name ?? null,
|
|
||||||
origin:
|
|
||||||
schedule.originStation?.label ?? schedule.originStation?.code ?? null,
|
|
||||||
destination:
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
wagonCount: schedule.trainSet?.wagonCount ?? 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) {
|
|
||||||
const schedule = await this.dataSource
|
|
||||||
.getRepository(TrainSchedule)
|
|
||||||
.findOne({
|
|
||||||
where: { id },
|
|
||||||
relations: {
|
|
||||||
route: true,
|
|
||||||
trainSet: {
|
|
||||||
locomotive: true,
|
|
||||||
wagons: { wagonType: true, physicalWagon: true, allocations: { booking: true } },
|
|
||||||
},
|
|
||||||
originStation: true,
|
|
||||||
destinationStation: true,
|
|
||||||
scheduleBookings: {
|
|
||||||
booking: { company: true, originYard: true, destinationYard: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!schedule) {
|
|
||||||
throw new NotFoundException(`Train schedule ${id} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: schedule.id,
|
|
||||||
status: schedule.status,
|
|
||||||
route: schedule.route
|
|
||||||
? {
|
|
||||||
id: schedule.route.id,
|
|
||||||
name: schedule.route.name,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
scheduledDepartureDate: schedule.scheduledDepartureDate,
|
|
||||||
scheduledArrivalDate: schedule.scheduledArrivalDate,
|
|
||||||
originStation: schedule.originStation,
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
maxTrainLengthMeters: this.roundTons(
|
|
||||||
Number(schedule.trainSet.locomotive.maxTrainLengthMeters),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: 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,
|
|
||||||
physicalWagon: wagon.physicalWagon
|
|
||||||
? {
|
|
||||||
id: wagon.physicalWagon.id,
|
|
||||||
wagonNumber: wagon.physicalWagon.wagonNumber,
|
|
||||||
status: wagon.physicalWagon.status,
|
|
||||||
}
|
|
||||||
: 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) => ({
|
|
||||||
id: scheduleBooking.booking?.id ?? scheduleBooking.bookingId,
|
|
||||||
reference: scheduleBooking.booking?.reference ?? null,
|
|
||||||
customer:
|
|
||||||
scheduleBooking.booking?.company?.name ??
|
|
||||||
scheduleBooking.booking?.company?.email ??
|
|
||||||
null,
|
|
||||||
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, wagons: true } },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!schedule) {
|
|
||||||
throw new NotFoundException(`Train schedule ${id} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.dataSource.transaction(async (manager) => {
|
|
||||||
await manager.getRepository(TrainSchedule).update(schedule.id, {
|
|
||||||
status: "CANCELLED",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (schedule.trainSetId) {
|
|
||||||
await manager.getRepository(TrainSet).update(schedule.trainSetId, {
|
|
||||||
status: "CANCELLED",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schedule.trainSet?.locomotiveId) {
|
|
||||||
await manager
|
|
||||||
.getRepository(Locomotive)
|
|
||||||
.update(schedule.trainSet.locomotiveId, {
|
|
||||||
status: "AVAILABLE",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const physicalWagonIds =
|
|
||||||
schedule.trainSet?.wagons
|
|
||||||
?.map((wagon) => wagon.physicalWagonId)
|
|
||||||
.filter((wagonId): wagonId is string => Boolean(wagonId)) ?? [];
|
|
||||||
|
|
||||||
if (physicalWagonIds.length > 0) {
|
|
||||||
const physicalWagons = await manager.getRepository(Wagon).find({
|
|
||||||
where: { id: In(physicalWagonIds) },
|
|
||||||
relations: { currentLocationYard: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
physicalWagons.map((wagon) =>
|
|
||||||
manager.getRepository(Wagon).update(wagon.id, {
|
|
||||||
status: this.expectedWagonStatusForYard(wagon.currentLocationYard),
|
|
||||||
sequenceNumber: null,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.getContainerTrainScheduleById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
async publishTrainSchedule(id: string) {
|
|
||||||
const schedule = await this.dataSource.getRepository(TrainSchedule).findOne({
|
|
||||||
where: { id },
|
|
||||||
relations: { trainSet: true, scheduleBookings: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!schedule) {
|
|
||||||
throw new NotFoundException(`Train schedule ${id} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (schedule.status === "CANCELLED") {
|
|
||||||
throw new BadRequestException("Cancelled schedules cannot be published");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!schedule.trainSet || schedule.trainSet.wagonCount <= 0) {
|
|
||||||
throw new BadRequestException("Allocate wagons before publishing the schedule");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((schedule.scheduleBookings?.length ?? 0) === 0) {
|
|
||||||
throw new BadRequestException("Assign bookings before publishing the schedule");
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.dataSource.getRepository(TrainSchedule).update(id, { status: "PUBLISHED" });
|
|
||||||
return this.getContainerTrainScheduleById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadBookingsForScheduling(bookingIds: string[]) {
|
|
||||||
return this.dataSource.getRepository(Booking).find({
|
|
||||||
where: { id: In(bookingIds) },
|
|
||||||
relations: {
|
|
||||||
company: true,
|
|
||||||
originYard: true,
|
|
||||||
destinationYard: true,
|
|
||||||
bookingContainers: { containerType: true },
|
|
||||||
},
|
|
||||||
order: { createdAt: "ASC" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
=======
|
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
private async getActiveRoute(routeId: string) {
|
private async getActiveRoute(routeId: string) {
|
||||||
const route = await this.dataSource.getRepository(Route).findOne({
|
const route = await this.dataSource.getRepository(Route).findOne({
|
||||||
where: { id: routeId },
|
where: { id: routeId },
|
||||||
@@ -2104,18 +1289,6 @@ export class TrainSchedulingService {
|
|||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
private expectedWagonStatusForYard(yard?: { country?: string } | null) {
|
|
||||||
const country = yard?.country?.trim().toLowerCase();
|
|
||||||
if (country === "ethiopia" || country === "et") return "EXPORT_READY";
|
|
||||||
if (country === "djibouti" || country === "djoubti" || country === "dj") return "IMPORT_READY";
|
|
||||||
return "AVAILABLE";
|
|
||||||
}
|
|
||||||
|
|
||||||
private toUtcDateKey(value: Date | string) {
|
|
||||||
const date = value instanceof Date ? value : new Date(value);
|
|
||||||
return date.toISOString().slice(0, 10);
|
|
||||||
=======
|
|
||||||
private mapEligibleBooking(booking: Booking) {
|
private mapEligibleBooking(booking: Booking) {
|
||||||
return {
|
return {
|
||||||
id: booking.id,
|
id: booking.id,
|
||||||
@@ -2137,7 +1310,6 @@ export class TrainSchedulingService {
|
|||||||
preferredDepartureDate: booking.scheduledDate.toISOString(),
|
preferredDepartureDate: booking.scheduledDate.toISOString(),
|
||||||
status: booking.status,
|
status: booking.status,
|
||||||
};
|
};
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveScheduleFreightType(
|
private resolveScheduleFreightType(
|
||||||
|
|||||||
@@ -52,13 +52,6 @@ export class TrainSetWagon extends BaseEntity {
|
|||||||
@Column({ name: 'assigned_weight_tons', type: 'numeric', precision: 10, scale: 3, default: 0 })
|
@Column({ name: 'assigned_weight_tons', type: 'numeric', precision: 10, scale: 3, default: 0 })
|
||||||
assignedWeightTons!: number;
|
assignedWeightTons!: number;
|
||||||
|
|
||||||
@Column({ name: 'physical_wagon_id', type: 'uuid', nullable: true })
|
|
||||||
physicalWagonId?: string | null;
|
|
||||||
|
|
||||||
@ManyToOne(() => Wagon, { nullable: true, onDelete: 'SET NULL' })
|
|
||||||
@JoinColumn({ name: 'physical_wagon_id' })
|
|
||||||
physicalWagon?: Wagon | null;
|
|
||||||
|
|
||||||
@Column({ name: 'status', type: 'varchar', length: 20, default: 'PLANNED' })
|
@Column({ name: 'status', type: 'varchar', length: 20, default: 'PLANNED' })
|
||||||
status!: string;
|
status!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ const toStringArray = ({ value }: { value: unknown }) => {
|
|||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
return value.map((entry) => String(entry).trim()).filter(Boolean);
|
||||||
}
|
}
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
|
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
if (typeof value !== 'string') return [];
|
if (typeof value !== 'string') return [];
|
||||||
|
|
||||||
return value
|
return value
|
||||||
@@ -46,40 +43,24 @@ export class CreateWagonTypeDto {
|
|||||||
@MaxLength(32)
|
@MaxLength(32)
|
||||||
code!: string;
|
code!: string;
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
@ApiProperty({ description: 'Display name, e.g. "Flat Wagon"', maxLength: 100 })
|
|
||||||
=======
|
|
||||||
@ApiProperty({ maxLength: 100, example: 'Flat wagon container' })
|
@ApiProperty({ maxLength: 100, example: 'Flat wagon container' })
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
@ApiProperty({ description: 'Maximum payload capacity in metric tons', example: 60 })
|
|
||||||
=======
|
|
||||||
@ApiProperty({ description: 'Maximum payload capacity in metric tons', example: 70 })
|
@ApiProperty({ description: 'Maximum payload capacity in metric tons', example: 70 })
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
@Transform(toNumber)
|
@Transform(toNumber)
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0.001)
|
@Min(0.001)
|
||||||
capacityTons!: number;
|
capacityTons!: number;
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
@ApiProperty({ description: 'Wagon length in meters', example: 14.2 })
|
|
||||||
=======
|
|
||||||
@ApiProperty({ description: 'Wagon length in meters', example: 14 })
|
@ApiProperty({ description: 'Wagon length in meters', example: 14 })
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
@Transform(toNumber)
|
@Transform(toNumber)
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0.001)
|
@Min(0.001)
|
||||||
lengthMeters!: number;
|
lengthMeters!: number;
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
@ApiPropertyOptional({ description: 'Maximum wagons of this type per train', example: 45 })
|
|
||||||
=======
|
|
||||||
@ApiPropertyOptional({ description: 'Maximum wagons of this type per train', example: 53 })
|
@ApiPropertyOptional({ description: 'Maximum wagons of this type per train', example: 53 })
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(toOptionalNumber)
|
@Transform(toOptionalNumber)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ export class WagonTypesController {
|
|||||||
@RuleEngineView('wagon-types')
|
@RuleEngineView('wagon-types')
|
||||||
@ApiOperation({ summary: 'List wagon types' })
|
@ApiOperation({ summary: 'List wagon types' })
|
||||||
findAll(@Query() query: Record<string, string | undefined>) {
|
findAll(@Query() query: Record<string, string | undefined>) {
|
||||||
<<<<<<< HEAD
|
|
||||||
return this.wagonTypesService.findAll(query);
|
|
||||||
=======
|
|
||||||
return this.wagonTypesService.findAll({
|
return this.wagonTypesService.findAll({
|
||||||
isActive:
|
isActive:
|
||||||
query.isActive === 'all'
|
query.isActive === 'all'
|
||||||
@@ -43,7 +40,6 @@ export class WagonTypesController {
|
|||||||
sortBy: query.sortBy,
|
sortBy: query.sortBy,
|
||||||
sortOrder: query.sortOrder,
|
sortOrder: query.sortOrder,
|
||||||
});
|
});
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
|||||||
@@ -6,44 +6,18 @@ import { UpdateWagonTypeDto } from './dto/update-wagon-type.dto';
|
|||||||
import { WagonType } from './entities/wagon-type.entity';
|
import { WagonType } from './entities/wagon-type.entity';
|
||||||
import { WagonTypesRepository } from './wagon-types.repository';
|
import { WagonTypesRepository } from './wagon-types.repository';
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
type WagonTypeListResponse = {
|
|
||||||
data: WagonType[];
|
|
||||||
meta: { total: number; page: number; pageSize: number; totalPages: number };
|
|
||||||
=======
|
|
||||||
type WagonTypeListFilter = {
|
type WagonTypeListFilter = {
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
sortBy?: string;
|
sortBy?: string;
|
||||||
sortOrder?: string;
|
sortOrder?: string;
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WagonTypesService {
|
export class WagonTypesService {
|
||||||
constructor(private readonly wagonTypesRepository: WagonTypesRepository) {}
|
constructor(private readonly wagonTypesRepository: WagonTypesRepository) {}
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
async findAll(query: Record<string, string | undefined> = {}): Promise<WagonTypeListResponse> {
|
|
||||||
const page = Math.max(1, Number(query.page) || 1);
|
|
||||||
const pageSize = Math.max(1, Number(query.pageSize) || 20);
|
|
||||||
const isActive =
|
|
||||||
query.isActive === 'all'
|
|
||||||
? undefined
|
|
||||||
: query.isActive === undefined
|
|
||||||
? true
|
|
||||||
: query.isActive === 'true';
|
|
||||||
const sortBy = ['code', 'name', 'capacityTons', 'lengthMeters', 'isActive'].includes(
|
|
||||||
query.sortBy ?? '',
|
|
||||||
)
|
|
||||||
? (query.sortBy as keyof WagonType)
|
|
||||||
: 'code';
|
|
||||||
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
|
||||||
|
|
||||||
const [data, total] = await this.wagonTypesRepository.findAndCount({
|
|
||||||
where: isActive === undefined ? {} : { isActive },
|
|
||||||
=======
|
|
||||||
async findAll(filter: WagonTypeListFilter = {}): Promise<{
|
async findAll(filter: WagonTypeListFilter = {}): Promise<{
|
||||||
data: WagonType[];
|
data: WagonType[];
|
||||||
meta: { total: number; page: number; pageSize: number; totalPages: number };
|
meta: { total: number; page: number; pageSize: number; totalPages: number };
|
||||||
@@ -59,7 +33,6 @@ export class WagonTypesService {
|
|||||||
|
|
||||||
const [data, total] = await this.wagonTypesRepository.findAndCount({
|
const [data, total] = await this.wagonTypesRepository.findAndCount({
|
||||||
where: filter.isActive === undefined ? {} : { isActive: filter.isActive },
|
where: filter.isActive === undefined ? {} : { isActive: filter.isActive },
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
order: { [sortBy]: sortOrder } as FindOptionsOrder<WagonType>,
|
order: { [sortBy]: sortOrder } as FindOptionsOrder<WagonType>,
|
||||||
skip: (page - 1) * pageSize,
|
skip: (page - 1) * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
@@ -71,21 +44,14 @@ export class WagonTypesService {
|
|||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
<<<<<<< HEAD
|
|
||||||
totalPages: Math.ceil(total / pageSize),
|
|
||||||
=======
|
|
||||||
totalPages: Math.max(1, Math.ceil(total / pageSize)),
|
totalPages: Math.max(1, Math.ceil(total / pageSize)),
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string): Promise<WagonType> {
|
async findById(id: string): Promise<WagonType> {
|
||||||
const wagonType = await this.wagonTypesRepository.findById(id);
|
const wagonType = await this.wagonTypesRepository.findById(id);
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
|
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
if (!wagonType) {
|
if (!wagonType) {
|
||||||
throw new NotFoundException(`Wagon type ${id} not found`);
|
throw new NotFoundException(`Wagon type ${id} not found`);
|
||||||
}
|
}
|
||||||
@@ -103,25 +69,16 @@ export class WagonTypesService {
|
|||||||
async create(dto: CreateWagonTypeDto): Promise<WagonType> {
|
async create(dto: CreateWagonTypeDto): Promise<WagonType> {
|
||||||
const code = dto.code.trim().toUpperCase();
|
const code = dto.code.trim().toUpperCase();
|
||||||
const existing = await this.wagonTypesRepository.findByCode(code);
|
const existing = await this.wagonTypesRepository.findByCode(code);
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
|
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
throw new ConflictException(`Wagon type code "${code}" already exists`);
|
throw new ConflictException(`Wagon type code "${code}" already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.wagonTypesRepository.create({
|
return this.wagonTypesRepository.create({
|
||||||
<<<<<<< HEAD
|
|
||||||
...dto,
|
|
||||||
code,
|
|
||||||
name: dto.name.trim(),
|
|
||||||
=======
|
|
||||||
code,
|
code,
|
||||||
name: dto.name.trim(),
|
name: dto.name.trim(),
|
||||||
capacityTons: dto.capacityTons,
|
capacityTons: dto.capacityTons,
|
||||||
lengthMeters: dto.lengthMeters,
|
lengthMeters: dto.lengthMeters,
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
maxWagonsPerTrain: dto.maxWagonsPerTrain ?? null,
|
maxWagonsPerTrain: dto.maxWagonsPerTrain ?? null,
|
||||||
supportedLoadTypes: dto.supportedLoadTypes ?? [],
|
supportedLoadTypes: dto.supportedLoadTypes ?? [],
|
||||||
isActive: dto.isActive ?? true,
|
isActive: dto.isActive ?? true,
|
||||||
@@ -143,12 +100,9 @@ export class WagonTypesService {
|
|||||||
...dto,
|
...dto,
|
||||||
...(nextCode ? { code: nextCode } : {}),
|
...(nextCode ? { code: nextCode } : {}),
|
||||||
...(dto.name ? { name: dto.name.trim() } : {}),
|
...(dto.name ? { name: dto.name.trim() } : {}),
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
maxWagonsPerTrain:
|
maxWagonsPerTrain:
|
||||||
dto.maxWagonsPerTrain === undefined ? undefined : dto.maxWagonsPerTrain ?? null,
|
dto.maxWagonsPerTrain === undefined ? undefined : dto.maxWagonsPerTrain ?? null,
|
||||||
supportedLoadTypes: dto.supportedLoadTypes ?? undefined,
|
supportedLoadTypes: dto.supportedLoadTypes ?? undefined,
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
|
|||||||
@@ -30,17 +30,12 @@ export class CreateWagonDto {
|
|||||||
maxPayloadWeight!: number;
|
maxPayloadWeight!: number;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
<<<<<<< HEAD
|
|
||||||
@IsIn(['AVAILABLE', 'IMPORT_READY', 'EXPORT_READY', 'ASSIGNED', 'MAINTENANCE', 'RETIRED'])
|
|
||||||
status?: string;
|
|
||||||
=======
|
|
||||||
@IsEnum(WagonStatus)
|
@IsEnum(WagonStatus)
|
||||||
status?: WagonStatus;
|
status?: WagonStatus;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsEnum(WagonReadiness)
|
@IsEnum(WagonReadiness)
|
||||||
readiness?: WagonReadiness;
|
readiness?: WagonReadiness;
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
|
|||||||
export const WAGON_STATUSES = [
|
export const WAGON_STATUSES = [
|
||||||
WagonStatus.Available,
|
WagonStatus.Available,
|
||||||
WagonStatus.Assigned,
|
WagonStatus.Assigned,
|
||||||
|
WagonStatus.ImportReady,
|
||||||
|
WagonStatus.ExportReady,
|
||||||
WagonStatus.Maintenance,
|
WagonStatus.Maintenance,
|
||||||
WagonStatus.Retired,
|
WagonStatus.Retired,
|
||||||
] as const;
|
] as const;
|
||||||
@@ -56,16 +58,11 @@ export class Wagon extends BaseEntity {
|
|||||||
@Column({ name: 'max_payload_weight', type: 'decimal', precision: 10, scale: 2 })
|
@Column({ name: 'max_payload_weight', type: 'decimal', precision: 10, scale: 2 })
|
||||||
maxPayloadWeight!: number;
|
maxPayloadWeight!: number;
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
@Column({ type: 'varchar', default: 'AVAILABLE' })
|
|
||||||
status!: string; // AVAILABLE, IMPORT_READY, EXPORT_READY, ASSIGNED, MAINTENANCE, RETIRED
|
|
||||||
=======
|
|
||||||
@Column({ type: 'varchar', length: 20, default: WagonStatus.Available })
|
@Column({ type: 'varchar', length: 20, default: WagonStatus.Available })
|
||||||
status!: WagonStatusType;
|
status!: WagonStatusType;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 20, default: WagonReadiness.ImportReady })
|
@Column({ type: 'varchar', length: 20, default: WagonReadiness.ImportReady })
|
||||||
readiness!: WagonReadinessType;
|
readiness!: WagonReadinessType;
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
notes!: string | null;
|
notes!: string | null;
|
||||||
@@ -84,7 +81,7 @@ export class Wagon extends BaseEntity {
|
|||||||
@JoinColumn({ name: 'current_train_schedule_id' })
|
@JoinColumn({ name: 'current_train_schedule_id' })
|
||||||
currentTrainSchedule?: TrainSchedule | null;
|
currentTrainSchedule?: TrainSchedule | null;
|
||||||
|
|
||||||
/** Fleet master consist grouping — separate from operational train_schedules. */
|
/** Fleet master consist grouping — separate from operational train_schedules. */
|
||||||
@ManyToOne(() => Train, (train) => train.wagons, { onDelete: 'SET NULL' })
|
@ManyToOne(() => Train, (train) => train.wagons, { onDelete: 'SET NULL' })
|
||||||
@JoinColumn({ name: 'train_id' })
|
@JoinColumn({ name: 'train_id' })
|
||||||
train!: Train | null;
|
train!: Train | null;
|
||||||
|
|||||||
@@ -41,26 +41,16 @@ export class WagonsService {
|
|||||||
const status = query.status?.trim();
|
const status = query.status?.trim();
|
||||||
const readiness = query.readiness?.trim();
|
const readiness = query.readiness?.trim();
|
||||||
const trainId = query.trainId?.trim();
|
const trainId = query.trainId?.trim();
|
||||||
<<<<<<< HEAD
|
|
||||||
const currentLocationYardId = query.currentLocationYardId?.trim();
|
|
||||||
=======
|
|
||||||
const filters = {
|
const filters = {
|
||||||
...(status ? { status: status as Wagon['status'] } : {}),
|
...(status ? { status: status as Wagon['status'] } : {}),
|
||||||
...(readiness ? { readiness: readiness as Wagon['readiness'] } : {}),
|
...(readiness ? { readiness: readiness as Wagon['readiness'] } : {}),
|
||||||
...(trainId ? { trainId } : {}),
|
...(trainId ? { trainId } : {}),
|
||||||
};
|
};
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
where.push({
|
where.push({
|
||||||
wagonNumber: ILike(`%${search}%`),
|
wagonNumber: ILike(`%${search}%`),
|
||||||
<<<<<<< HEAD
|
|
||||||
...(status ? { status } : {}),
|
|
||||||
...(trainId ? { trainId } : {}),
|
|
||||||
...(currentLocationYardId ? { currentLocationYardId } : {}),
|
|
||||||
=======
|
|
||||||
...filters,
|
...filters,
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,12 +60,7 @@ export class WagonsService {
|
|||||||
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
||||||
|
|
||||||
return this.wagonRepo.find({
|
return this.wagonRepo.find({
|
||||||
<<<<<<< HEAD
|
|
||||||
where: search ? where : { ...(status ? { status } : {}), ...(trainId ? { trainId } : {}), ...(currentLocationYardId ? { currentLocationYardId } : {}) },
|
|
||||||
relations: { currentLocationYard: true, wagonType: true },
|
|
||||||
=======
|
|
||||||
where: search ? where : filters,
|
where: search ? where : filters,
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
order: { [sortBy]: sortOrder } as FindOptionsOrder<Wagon>,
|
order: { [sortBy]: sortOrder } as FindOptionsOrder<Wagon>,
|
||||||
skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined,
|
skip: query.page && query.limit ? (Number(query.page) - 1) * Number(query.limit) : undefined,
|
||||||
take: query.limit ? Number(query.limit) : undefined,
|
take: query.limit ? Number(query.limit) : undefined,
|
||||||
@@ -131,22 +116,23 @@ export class WagonsService {
|
|||||||
const wagon = await this.findById(wagonId);
|
const wagon = await this.findById(wagonId);
|
||||||
wagon.trainId = null;
|
wagon.trainId = null;
|
||||||
wagon.sequenceNumber = null;
|
wagon.sequenceNumber = null;
|
||||||
<<<<<<< HEAD
|
|
||||||
wagon.status = await this.statusForLocation(wagon.currentLocationYardId, 'AVAILABLE');
|
|
||||||
=======
|
|
||||||
wagon.status = WagonStatus.Available;
|
wagon.status = WagonStatus.Available;
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
return this.wagonRepo.save(wagon);
|
return this.wagonRepo.save(wagon);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async statusForLocation(yardId?: string | null, fallback = 'AVAILABLE') {
|
private async statusForLocation(
|
||||||
|
yardId?: string | null,
|
||||||
|
fallback: Wagon['status'] = WagonStatus.Available,
|
||||||
|
): Promise<Wagon['status']> {
|
||||||
if (!yardId) return fallback;
|
if (!yardId) return fallback;
|
||||||
|
|
||||||
const yard = await this.yardRepo.findOne({ where: { id: yardId } });
|
const yard = await this.yardRepo.findOne({ where: { id: yardId } });
|
||||||
const country = yard?.country?.trim().toLowerCase();
|
const country = yard?.country?.trim().toLowerCase();
|
||||||
|
|
||||||
if (country === 'ethiopia' || country === 'et') return 'EXPORT_READY';
|
if (country === 'ethiopia' || country === 'et') return WagonStatus.ExportReady;
|
||||||
if (country === 'djibouti' || country === 'djoubti' || country === 'dj') return 'IMPORT_READY';
|
if (country === 'djibouti' || country === 'djoubti' || country === 'dj') {
|
||||||
|
return WagonStatus.ImportReady;
|
||||||
|
}
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,24 +33,14 @@ import UserManagementPage from "./pages/dashboard/user-management/UserManagement
|
|||||||
import UsersPage from "./pages/dashboard/user-management/UsersPage";
|
import UsersPage from "./pages/dashboard/user-management/UsersPage";
|
||||||
import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage";
|
import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage";
|
||||||
import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage";
|
import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage";
|
||||||
import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirect";
|
import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirect";
|
||||||
import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage";
|
import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage";
|
||||||
import TrainsPage from "./pages/trains/TrainsPage";
|
import TrainsPage from "./pages/trains/TrainsPage";
|
||||||
<<<<<<< HEAD
|
import TrainScheduleV2ListPage from "./pages/trainScheduling/TrainScheduleV2ListPage";
|
||||||
import {
|
import TrainScheduleV2DetailPage from "./pages/trainScheduling/TrainScheduleV2DetailPage";
|
||||||
CargoesCrudPage,
|
import TrainSchedulingGlobalRulesPage from "./pages/trainScheduling/TrainSchedulingGlobalRulesPage";
|
||||||
ContainersCrudPage,
|
import FleetResourcePage from "./pages/fleet/FleetResourcePage";
|
||||||
LocomotivesCrudPage,
|
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
|
||||||
TrainMasterDataPage,
|
|
||||||
WagonsCrudPage,
|
|
||||||
} from "./pages/fleet/FleetCrudPages";
|
|
||||||
=======
|
|
||||||
import TrainScheduleV2ListPage from "./pages/trainScheduling/TrainScheduleV2ListPage";
|
|
||||||
import TrainScheduleV2DetailPage from "./pages/trainScheduling/TrainScheduleV2DetailPage";
|
|
||||||
import TrainSchedulingGlobalRulesPage from "./pages/trainScheduling/TrainSchedulingGlobalRulesPage";
|
|
||||||
import FleetResourcePage from "./pages/fleet/FleetResourcePage";
|
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
|
|
||||||
import TrainDetailPage from "./pages/trains/TrainDetailPage";
|
import TrainDetailPage from "./pages/trains/TrainDetailPage";
|
||||||
import RoutesPage from "./pages/fleet/RoutesPage";
|
import RoutesPage from "./pages/fleet/RoutesPage";
|
||||||
import WarehouseListPage from "./pages/warehouses/WarehouseListPage";
|
import WarehouseListPage from "./pages/warehouses/WarehouseListPage";
|
||||||
@@ -289,35 +279,24 @@ const App = () => {
|
|||||||
path="booking-requests/:id/contract"
|
path="booking-requests/:id/contract"
|
||||||
element={<BookingContractPage />}
|
element={<BookingContractPage />}
|
||||||
/>
|
/>
|
||||||
<Route path="operations/train-scheduling" element={<TrainsPage />} />
|
<Route path="operations/train-scheduling" element={<TrainsPage />} />
|
||||||
<<<<<<< HEAD
|
<Route
|
||||||
<Route path="trains" element={<TrainMasterDataPage />} />
|
path="operations/train-scheduling-v2"
|
||||||
<Route path="operations/train-scheduling" element={<TrainsPage />} />
|
element={<TrainScheduleV2ListPage />}
|
||||||
=======
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="operations/train-scheduling-v2"
|
path="operations/train-scheduling-v2/:scheduleId"
|
||||||
element={<TrainScheduleV2ListPage />}
|
element={<TrainScheduleV2DetailPage />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route path="routes" element={<RoutesPage />} />
|
||||||
path="operations/train-scheduling-v2/:scheduleId"
|
<Route path="locomotives" element={<FleetResourcePage />} />
|
||||||
element={<TrainScheduleV2DetailPage />}
|
<Route path="trains" element={<FleetResourcePage />} />
|
||||||
/>
|
<Route path="trains/:id" element={<TrainDetailPage />} />
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
<Route path="wagons" element={<FleetResourcePage />} />
|
||||||
<Route path="routes" element={<RoutesPage />} />
|
<Route path="containers" element={<FleetResourcePage />} />
|
||||||
<Route path="locomotives" element={<FleetResourcePage />} />
|
<Route path="cargoes" element={<FleetResourcePage />} />
|
||||||
<Route path="trains" element={<FleetResourcePage />} />
|
|
||||||
<Route path="trains/:id" element={<TrainDetailPage />} />
|
<Route path="warehouses" element={<WarehouseListPage />} />
|
||||||
<<<<<<< HEAD
|
|
||||||
<Route path="wagons" element={<WagonsCrudPage />} />
|
|
||||||
<Route path="containers" element={<ContainersCrudPage />} />
|
|
||||||
<Route path="cargoes" element={<CargoesCrudPage />} />
|
|
||||||
=======
|
|
||||||
<Route path="wagons" element={<FleetResourcePage />} />
|
|
||||||
<Route path="containers" element={<FleetResourcePage />} />
|
|
||||||
<Route path="cargoes" element={<FleetResourcePage />} />
|
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
|
|
||||||
<Route path="warehouses" element={<WarehouseListPage />} />
|
|
||||||
<Route path="warehouses/:id" element={<WarehouseDetailPage />} />
|
<Route path="warehouses/:id" element={<WarehouseDetailPage />} />
|
||||||
<Route path="warehouse-inventory" element={<WarehouseInventoryPage />} />
|
<Route path="warehouse-inventory" element={<WarehouseInventoryPage />} />
|
||||||
<Route path="inventory-inquiry" element={<InventoryInquiryPage />} />
|
<Route path="inventory-inquiry" element={<InventoryInquiryPage />} />
|
||||||
|
|||||||
@@ -19,11 +19,7 @@ const asList = <T>(payload: ListResponse<T>): T[] =>
|
|||||||
export const wagonTypesService = {
|
export const wagonTypesService = {
|
||||||
async getWagonTypes() {
|
async getWagonTypes() {
|
||||||
const response = await api.get<ListResponse<WagonType>>('/wagon-types', {
|
const response = await api.get<ListResponse<WagonType>>('/wagon-types', {
|
||||||
<<<<<<< HEAD
|
|
||||||
params: { isActive: 'all' },
|
|
||||||
=======
|
|
||||||
params: { isActive: 'all', pageSize: 500 },
|
params: { isActive: 'all', pageSize: 500 },
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
});
|
});
|
||||||
return asList(response.data);
|
return asList(response.data);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -136,20 +136,15 @@ export interface LocomotiveRecord {
|
|||||||
name?: string | null;
|
name?: string | null;
|
||||||
maxPullWeightTons: number;
|
maxPullWeightTons: number;
|
||||||
maxTrainLengthMeters: number;
|
maxTrainLengthMeters: number;
|
||||||
<<<<<<< HEAD
|
|
||||||
status:
|
status:
|
||||||
| 'AVAILABLE'
|
| "AVAILABLE"
|
||||||
| 'UNAVAILABLE'
|
| "UNAVAILABLE"
|
||||||
| 'IMPORT_READY'
|
| "IMPORT_READY"
|
||||||
| 'EXPORT_READY'
|
| "EXPORT_READY"
|
||||||
| 'ASSIGNED'
|
| "ASSIGNED"
|
||||||
| 'MAINTENANCE'
|
| "MAINTENANCE"
|
||||||
| 'OUT_OF_SERVICE';
|
| "OUT_OF_SERVICE";
|
||||||
locomotiveType?: 'DIESEL' | 'ELECTRIC';
|
|
||||||
=======
|
|
||||||
status: "AVAILABLE" | "ASSIGNED" | "MAINTENANCE" | "OUT_OF_SERVICE";
|
|
||||||
locomotiveType?: "DIESEL" | "ELECTRIC";
|
locomotiveType?: "DIESEL" | "ELECTRIC";
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrainScheduleListItem {
|
export interface TrainScheduleListItem {
|
||||||
@@ -250,21 +245,12 @@ export interface TrainScheduleDetail {
|
|||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
} | null;
|
} | null;
|
||||||
<<<<<<< HEAD
|
|
||||||
physicalWagon?: {
|
physicalWagon?: {
|
||||||
id: string;
|
id: string;
|
||||||
wagonNumber: string;
|
wagonNumber: string;
|
||||||
status: string;
|
status: string;
|
||||||
} | null;
|
} | null;
|
||||||
allocations: Array<{
|
|
||||||
id: string;
|
|
||||||
bookingId: string;
|
|
||||||
bookingReference: string | null;
|
|
||||||
allocatedWeightTons: number;
|
|
||||||
}>;
|
|
||||||
=======
|
|
||||||
allocations: TrainScheduleWagonAllocation[];
|
allocations: TrainScheduleWagonAllocation[];
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}>;
|
}>;
|
||||||
} | null;
|
} | null;
|
||||||
bookings: Array<{
|
bookings: Array<{
|
||||||
@@ -281,13 +267,10 @@ export interface TrainScheduleDetail {
|
|||||||
export interface TrainScheduleFilters {
|
export interface TrainScheduleFilters {
|
||||||
originStationId?: string;
|
originStationId?: string;
|
||||||
destinationStationId?: string;
|
destinationStationId?: string;
|
||||||
<<<<<<< HEAD
|
|
||||||
scheduleDate?: string;
|
scheduleDate?: string;
|
||||||
assignmentType?: AssignmentType;
|
assignmentType?: AssignmentType;
|
||||||
tradeDirection?: TradeDirection;
|
tradeDirection?: TradeDirection;
|
||||||
=======
|
|
||||||
schedulingStatus?: SchedulingStatus;
|
schedulingStatus?: SchedulingStatus;
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TrainSchedulePreviewPayload {
|
export interface TrainSchedulePreviewPayload {
|
||||||
@@ -295,9 +278,7 @@ export interface TrainSchedulePreviewPayload {
|
|||||||
scheduleDate: string;
|
scheduleDate: string;
|
||||||
originStationId: string;
|
originStationId: string;
|
||||||
destinationStationId: string;
|
destinationStationId: string;
|
||||||
<<<<<<< HEAD
|
|
||||||
assignmentType?: AssignmentType;
|
assignmentType?: AssignmentType;
|
||||||
=======
|
|
||||||
targetScheduleId?: string;
|
targetScheduleId?: string;
|
||||||
maxTrainWeightTons?: number;
|
maxTrainWeightTons?: number;
|
||||||
maxTrainLengthMeters?: number;
|
maxTrainLengthMeters?: number;
|
||||||
@@ -320,7 +301,6 @@ export interface ReschedulePlan {
|
|||||||
readmitted: RescheduleBookingSummary[];
|
readmitted: RescheduleBookingSummary[];
|
||||||
finalBookingIds: string[];
|
finalBookingIds: string[];
|
||||||
warnings: string[];
|
warnings: string[];
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateTrainSchedulePayload {
|
export interface CreateTrainSchedulePayload {
|
||||||
@@ -328,11 +308,9 @@ export interface CreateTrainSchedulePayload {
|
|||||||
scheduleDate: string;
|
scheduleDate: string;
|
||||||
arrivalDate?: string;
|
arrivalDate?: string;
|
||||||
locomotiveId: string;
|
locomotiveId: string;
|
||||||
<<<<<<< HEAD
|
|
||||||
assignmentType?: AssignmentType;
|
assignmentType?: AssignmentType;
|
||||||
bookingIds: string[];
|
bookingIds: string[];
|
||||||
wagonIds?: string[];
|
wagonIds?: string[];
|
||||||
=======
|
|
||||||
maxTrainWeightTons?: number;
|
maxTrainWeightTons?: number;
|
||||||
maxTrainLengthMeters?: number;
|
maxTrainLengthMeters?: number;
|
||||||
maxWagonsPerTrain?: number;
|
maxWagonsPerTrain?: number;
|
||||||
@@ -354,5 +332,4 @@ export interface PinWagonAssignment {
|
|||||||
|
|
||||||
export interface PinWagonsPayload {
|
export interface PinWagonsPayload {
|
||||||
assignments: PinWagonAssignment[];
|
assignments: PinWagonAssignment[];
|
||||||
>>>>>>> 10b011de8feafce5c0bae21594df5b36596587db
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ export enum TrainSetWagonStatus {
|
|||||||
export enum WagonStatus {
|
export enum WagonStatus {
|
||||||
Available = "AVAILABLE",
|
Available = "AVAILABLE",
|
||||||
Assigned = "ASSIGNED",
|
Assigned = "ASSIGNED",
|
||||||
|
ImportReady = "IMPORT_READY",
|
||||||
|
ExportReady = "EXPORT_READY",
|
||||||
Maintenance = "MAINTENANCE",
|
Maintenance = "MAINTENANCE",
|
||||||
Retired = "RETIRED",
|
Retired = "RETIRED",
|
||||||
}
|
}
|
||||||
@@ -335,7 +337,7 @@ export interface IInvoice extends BaseEntity {
|
|||||||
dueAt: string;
|
dueAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Reference Data (booking form catalog) ──────────────────────────────────────
|
// ── Reference Data (booking form catalog) ──────────────────────────────────────
|
||||||
|
|
||||||
export interface BookingReferenceYard {
|
export interface BookingReferenceYard {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -391,7 +393,7 @@ export interface BookingReferenceData {
|
|||||||
cargo_type: BookingReferenceCargoTypeGroup[];
|
cargo_type: BookingReferenceCargoTypeGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── DTOs ───────────────────────────────────────────────────────────────────────
|
// ── DTOs ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface CreateBookingContainerDto {
|
export interface CreateBookingContainerDto {
|
||||||
containerTypeId: string;
|
containerTypeId: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://turbo.build/schema.json",
|
"$schema": "https://turbo.build/schema.json",
|
||||||
|
"concurrency": "11",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"build": {
|
"build": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
|
|||||||
Reference in New Issue
Block a user