mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 04:50:54 +00:00
add items per wagon map to cargo types and sync bulk rate units
- Implemented in the to manage the physical item capacity for each wagon type. - Added a new migration to create the column in the table. - Introduced method in to update rate units when cargo type unit of measure changes. - Updated booking calculations to consider items per wagon for break-bulk cargo. - Refactored various components to utilize the new items fit logic and ensure consistent date formatting across the application. - Added tests for the new display timezone functionality to ensure consistent date/time representation across different user settings.
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
import { PaginatedResponse } from '@edr/types';
|
||||
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CargoUnitOfMeasure, PaginatedResponse } from '@edr/types';
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
Inject,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { generateCode } from '../../../common/utils/generate-code.util';
|
||||
import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto';
|
||||
import { ListCargoTypesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
@@ -11,6 +17,7 @@ import {
|
||||
CARGO_TYPES_REPOSITORY,
|
||||
ICargoTypesRepository,
|
||||
} from '../interfaces/cargo-types.repository.interface';
|
||||
import { IRatesRepository, RATES_REPOSITORY } from '../interfaces/rates.repository.interface';
|
||||
import { DisplayOrderService } from './display-order.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -18,6 +25,8 @@ export class CargoTypesService {
|
||||
constructor(
|
||||
@Inject(CARGO_TYPES_REPOSITORY)
|
||||
private readonly repository: ICargoTypesRepository,
|
||||
@Inject(RATES_REPOSITORY)
|
||||
private readonly ratesRepository: IRatesRepository,
|
||||
private readonly displayOrder: DisplayOrderService,
|
||||
) {}
|
||||
|
||||
@@ -38,6 +47,34 @@ export class CargoTypesService {
|
||||
return this.repository.findByCode(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* PER_ITEM (break-bulk) cargo must carry a whole-items-fit for EVERY allowed
|
||||
* wagon type — allocation caps each wagon at min(fit, tonnage) and a missing
|
||||
* fit would silently fall back to tonnage-only loading. Returns the map
|
||||
* trimmed to the allowed ids (stale keys from a removed wagon type drop out);
|
||||
* null when the cargo is not PER_ITEM or has no wagon types.
|
||||
*/
|
||||
private resolveItemsPerWagonMap(input: {
|
||||
unitOfMeasure?: CargoUnitOfMeasure | null;
|
||||
wagonTypeIds: string[];
|
||||
itemsPerWagonMap?: Record<string, number> | null;
|
||||
}): Record<string, number> | null {
|
||||
if (input.unitOfMeasure !== CargoUnitOfMeasure.PerItem || !input.wagonTypeIds.length) {
|
||||
return null;
|
||||
}
|
||||
const map: Record<string, number> = {};
|
||||
for (const wagonTypeId of input.wagonTypeIds) {
|
||||
const fit = Number(input.itemsPerWagonMap?.[wagonTypeId]);
|
||||
if (!Number.isInteger(fit) || fit < 1) {
|
||||
throw new BadRequestException(
|
||||
`itemsPerWagonMap must define how many items fit wagon type ${wagonTypeId} (integer >= 1) for PER_ITEM cargo`,
|
||||
);
|
||||
}
|
||||
map[wagonTypeId] = fit;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Create a new cargo type. */
|
||||
async create(dto: CreateCargoTypeDto): Promise<CargoType> {
|
||||
const code = generateCode(dto.cargoTypeName);
|
||||
@@ -62,26 +99,58 @@ export class CargoTypesService {
|
||||
unitOfMeasure: dto.unitOfMeasure ?? null,
|
||||
// Join rows are written by the save (RESTRICT FK rejects unknown ids).
|
||||
wagonTypes: (dto.wagonTypeIds ?? []).map((id) => ({ id }) as WagonType),
|
||||
itemsPerWagonMap: this.resolveItemsPerWagonMap({
|
||||
unitOfMeasure: dto.unitOfMeasure ?? null,
|
||||
wagonTypeIds: dto.wagonTypeIds ?? [],
|
||||
itemsPerWagonMap: dto.itemsPerWagonMap,
|
||||
}),
|
||||
displayOrder,
|
||||
});
|
||||
}
|
||||
|
||||
/** Update an existing cargo type. */
|
||||
async update(id: string, dto: UpdateCargoTypeDto): Promise<CargoType> {
|
||||
await this.findById(id);
|
||||
const existing = await this.findById(id);
|
||||
if (dto.parentGroupId) {
|
||||
if (dto.parentGroupId === id) throw new ConflictException('A cargo type cannot be its own parent');
|
||||
const parent = await this.repository.findById(dto.parentGroupId);
|
||||
if (!parent) throw new NotFoundException(`Parent cargo type ${dto.parentGroupId} not found`);
|
||||
}
|
||||
const { wagonTypeIds, insertAfterId: _insertAfterId, ...columns } = dto;
|
||||
const { wagonTypeIds, itemsPerWagonMap, insertAfterId: _insertAfterId, ...columns } = dto;
|
||||
// Re-validate the fit map whenever anything it depends on moves — a partial
|
||||
// update merges with the stored values so e.g. adding a wagon type without
|
||||
// its fit still 400s. Untouched fields leave the stored map alone.
|
||||
const touchesItemsFit =
|
||||
wagonTypeIds !== undefined || itemsPerWagonMap !== undefined || dto.unitOfMeasure !== undefined;
|
||||
const updated = await this.repository.update(id, {
|
||||
...columns,
|
||||
...(wagonTypeIds
|
||||
? { wagonTypes: wagonTypeIds.map((wagonTypeId) => ({ id: wagonTypeId }) as WagonType) }
|
||||
: {}),
|
||||
...(touchesItemsFit
|
||||
? {
|
||||
itemsPerWagonMap: this.resolveItemsPerWagonMap({
|
||||
unitOfMeasure:
|
||||
dto.unitOfMeasure !== undefined ? dto.unitOfMeasure : existing.unitOfMeasure,
|
||||
wagonTypeIds: wagonTypeIds ?? (existing.wagonTypes ?? []).map((wt) => wt.id),
|
||||
itemsPerWagonMap:
|
||||
itemsPerWagonMap !== undefined ? itemsPerWagonMap : existing.itemsPerWagonMap,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
if (!updated) throw new NotFoundException(`Cargo type ${id} not found`);
|
||||
// A uom flip renames how existing rates bill (PER_TON ↔ PER_ITEM name the
|
||||
// same stored quantity) — sync them or bookings keep quoting "per ton" for
|
||||
// counted cargo.
|
||||
if (
|
||||
dto.unitOfMeasure !== undefined &&
|
||||
dto.unitOfMeasure !== existing.unitOfMeasure &&
|
||||
(dto.unitOfMeasure === CargoUnitOfMeasure.PerTon ||
|
||||
dto.unitOfMeasure === CargoUnitOfMeasure.PerItem)
|
||||
) {
|
||||
await this.ratesRepository.syncBulkQuantityUnit(id, dto.unitOfMeasure);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user