mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
update joins in repository services to use entity classes and enhance booking form with hazardous/reefer toggles
This commit is contained in:
@@ -15,7 +15,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource, EntityManager, In, Not } from 'typeorm';
|
||||
import { DataSource, EntityManager, In, IsNull, Not } from 'typeorm';
|
||||
|
||||
import { BookingsRepository } from '../bookings/bookings.repository';
|
||||
import { Booking } from '../bookings/entities/booking.entity';
|
||||
@@ -38,6 +38,8 @@ import { WagonAllocationBulkLoadsRepository } from '../train-schedules/wagon-all
|
||||
import { WagonAllocationContainerItemsRepository } from '../train-schedules/wagon-allocation-container-items.repository';
|
||||
import { WagonBookingAllocationsRepository } from '../train-schedules/wagon-booking-allocations.repository';
|
||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||
import { CargoType } from '../rule-engine/entities/cargo-type.entity';
|
||||
import { ContainerType } from '../rule-engine/entities/container-type.entity';
|
||||
import { WagonTypesRepository } from '../wagon-types/wagon-types.repository';
|
||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||
import { AssignBookingsDto } from './dto/assign-bookings.dto';
|
||||
@@ -87,10 +89,6 @@ import {
|
||||
type ContainerPlacementInput,
|
||||
type WagonPlanSlot,
|
||||
} from './wagon-plan.util';
|
||||
import {
|
||||
getDefaultContainerWagonTypeCode,
|
||||
pickBulkWagonType,
|
||||
} from './wagon-type-resolver.util';
|
||||
import { deriveScheduleDirection } from './derive-schedule-direction.util';
|
||||
import { pickLowestFreeNumber, pickTrainNumberPool } from './train-number.util';
|
||||
import {
|
||||
@@ -2725,28 +2723,98 @@ export class TrainSchedulingService {
|
||||
return violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the wagon type for a batch through the cargo-type / container-type
|
||||
* `wagon_type_id` FK (replaces the former load-type string matching). Throws
|
||||
* when the relevant type has no wagon type configured — scheduling is blocked
|
||||
* until an admin assigns one on the cargo-type / container-type config screen.
|
||||
*/
|
||||
private async resolveWagonType(
|
||||
freightType: 'CONTAINER' | 'BULK',
|
||||
bookingIds: string[],
|
||||
): Promise<WagonType> {
|
||||
const bookings = await this.bookingsRepository.findByIdsForScheduling(bookingIds);
|
||||
|
||||
if (freightType === 'CONTAINER') {
|
||||
const [wagonType] = await this.wagonTypesRepository.findAll({
|
||||
where: { code: getDefaultContainerWagonTypeCode(), isActive: true },
|
||||
});
|
||||
if (!wagonType) {
|
||||
throw new NotFoundException(`Wagon type ${getDefaultContainerWagonTypeCode()} not found`);
|
||||
// First container type present on the batch drives the container wagon
|
||||
// type (matches the prior single-wagon-type-per-consist behavior).
|
||||
const containerType = bookings
|
||||
.flatMap((b) => b.bookingContainers ?? [])
|
||||
.map((line) => line.containerType)
|
||||
.find((ct): ct is NonNullable<typeof ct> => Boolean(ct));
|
||||
if (!containerType) {
|
||||
throw new BadRequestException('No container type found on the container booking(s)');
|
||||
}
|
||||
const wagonType = await this.loadWagonTypeForType(
|
||||
containerType.wagonTypeId ?? null,
|
||||
`Container type "${containerType.label ?? containerType.code}"`,
|
||||
);
|
||||
return wagonType;
|
||||
}
|
||||
|
||||
const bookings = await this.bookingsRepository.findByIdsForScheduling(bookingIds);
|
||||
const cargoCode = bookings[0]?.cargoType?.code ?? null;
|
||||
const wagonTypes = await this.wagonTypesRepository.findAll({ where: { isActive: true } });
|
||||
const picked = pickBulkWagonType(wagonTypes, cargoCode);
|
||||
if (!picked) {
|
||||
throw new NotFoundException('No suitable bulk wagon type found');
|
||||
const cargoType = bookings.map((b) => b.cargoType).find((ct) => Boolean(ct));
|
||||
if (!cargoType) {
|
||||
throw new BadRequestException('No cargo type found on the bulk booking(s)');
|
||||
}
|
||||
return picked;
|
||||
return this.loadWagonTypeForType(
|
||||
cargoType.wagonTypeId ?? null,
|
||||
`Cargo type "${cargoType.cargoTypeName ?? cargoType.code}"`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an active wagon type by FK id, throwing a clear error when the id is
|
||||
* unset (type not configured) or points at a missing/inactive wagon type.
|
||||
*/
|
||||
private async loadWagonTypeForType(
|
||||
wagonTypeId: string | null,
|
||||
typeLabel: string,
|
||||
): Promise<WagonType> {
|
||||
if (!wagonTypeId) {
|
||||
throw new BadRequestException(
|
||||
`${typeLabel} has no wagon type configured — set one on its configuration before scheduling.`,
|
||||
);
|
||||
}
|
||||
const [wagonType] = await this.wagonTypesRepository.findAll({
|
||||
where: { id: wagonTypeId, isActive: true },
|
||||
});
|
||||
if (!wagonType) {
|
||||
throw new NotFoundException(
|
||||
`${typeLabel} references wagon type ${wagonTypeId}, which was not found or is inactive.`,
|
||||
);
|
||||
}
|
||||
return wagonType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft wagon-type resolution for the customer-facing availability preview
|
||||
* (getAvailableDaysForCargo). Reads the configured FK by cargo/container type;
|
||||
* returns null (→ "no days") instead of throwing when nothing is configured,
|
||||
* since this only estimates which days have wagons and creates no booking.
|
||||
*/
|
||||
private async resolveWagonTypeForPreview(
|
||||
freightType: 'CONTAINER' | 'BULK',
|
||||
cargoTypeCode: string | null,
|
||||
): Promise<WagonType | null> {
|
||||
if (freightType === 'BULK') {
|
||||
if (!cargoTypeCode) return null;
|
||||
const cargoType = await this.dataSource.getRepository(CargoType).findOne({
|
||||
where: { code: cargoTypeCode },
|
||||
relations: { wagonType: true },
|
||||
});
|
||||
return cargoType?.wagonType?.isActive ? cargoType.wagonType : null;
|
||||
}
|
||||
|
||||
// Container preview: the input carries no specific container type, so use the
|
||||
// wagon type of the first configured (active) container type.
|
||||
const containerType = await this.dataSource
|
||||
.getRepository(ContainerType)
|
||||
.findOne({
|
||||
where: { isActive: true, wagonTypeId: Not(IsNull()) },
|
||||
relations: { wagonType: true },
|
||||
order: { displayOrder: 'ASC' },
|
||||
});
|
||||
return containerType?.wagonType?.isActive ? containerType.wagonType : null;
|
||||
}
|
||||
|
||||
private async persistTrainSetWagons(
|
||||
@@ -3419,15 +3487,12 @@ export class TrainSchedulingService {
|
||||
);
|
||||
if (schedules.length === 0) return { days: [] };
|
||||
|
||||
const wagonTypes = await this.dataSource.getRepository(WagonType).find();
|
||||
|
||||
// Resolve the wagon type this cargo needs.
|
||||
const requiredType =
|
||||
input.freightType === 'BULK'
|
||||
? pickBulkWagonType(wagonTypes, input.cargoTypeCode)
|
||||
: wagonTypes.find(
|
||||
(wt) => wt.code === getDefaultContainerWagonTypeCode() && wt.isActive,
|
||||
);
|
||||
// Resolve the wagon type this cargo needs via the cargo/container-type FK.
|
||||
// Soft (customer availability preview): no days if unresolved, never throws.
|
||||
const requiredType = await this.resolveWagonTypeForPreview(
|
||||
input.freightType,
|
||||
input.cargoTypeCode ?? null,
|
||||
);
|
||||
if (!requiredType) return { days: [] };
|
||||
|
||||
// How many wagons of that type the cargo needs.
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||
|
||||
const CARGO_CODE_TO_WAGON_TYPE: Record<string, string> = {
|
||||
COFFEE: 'KW2',
|
||||
GRAIN: 'KW2',
|
||||
WHEAT: 'KW2',
|
||||
SORGHUM: 'KW2',
|
||||
CORN: 'KW2',
|
||||
FERTILIZER: 'PW2',
|
||||
SUGAR: 'PW2',
|
||||
COAL: 'KW3',
|
||||
STEEL: 'CW3',
|
||||
ORE: 'CW3',
|
||||
};
|
||||
|
||||
const DEFAULT_BULK_WAGON_TYPE = 'CW3';
|
||||
const DEFAULT_CONTAINER_WAGON_TYPE = 'NW5';
|
||||
|
||||
/**
|
||||
* Resolve wagon type code from cargo type code for bulk freight.
|
||||
*/
|
||||
export function resolveBulkWagonTypeCode(cargoTypeCode?: string | null): string {
|
||||
if (!cargoTypeCode) return DEFAULT_BULK_WAGON_TYPE;
|
||||
const normalized = cargoTypeCode.trim().toUpperCase();
|
||||
return CARGO_CODE_TO_WAGON_TYPE[normalized] ?? DEFAULT_BULK_WAGON_TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the best matching wagon type entity for bulk cargo.
|
||||
*/
|
||||
export function pickBulkWagonType(
|
||||
wagonTypes: WagonType[],
|
||||
cargoTypeCode?: string | null,
|
||||
): WagonType | undefined {
|
||||
const preferredCode = resolveBulkWagonTypeCode(cargoTypeCode);
|
||||
const direct = wagonTypes.find((wt) => wt.code === preferredCode && wt.isActive);
|
||||
if (direct) return direct;
|
||||
|
||||
return wagonTypes.find(
|
||||
(wt) =>
|
||||
wt.isActive &&
|
||||
!wt.supportsContainer &&
|
||||
wt.code !== DEFAULT_CONTAINER_WAGON_TYPE,
|
||||
);
|
||||
}
|
||||
|
||||
export function getDefaultContainerWagonTypeCode(): string {
|
||||
return DEFAULT_CONTAINER_WAGON_TYPE;
|
||||
}
|
||||
Reference in New Issue
Block a user