update joins in repository services to use entity classes and enhance booking form with hazardous/reefer toggles

This commit is contained in:
Marshal
2026-07-04 04:59:51 +00:00
parent c650a2dcbd
commit f37078d51d
21 changed files with 478 additions and 86 deletions

View File

@@ -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.