mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(warehouses): enforce yard/zone capacity limits; broaden dispatcher perms
Yard create/update now rejects capacities that overflow the parent warehouse, and zone create/update rejects capacities that overflow the parent yard, via BadRequestException. Dispatcher permission preset expanded to full CRUD on warehouse and fleet management; allocation and fee rules remain view-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { CreateWarehouseYardDto } from './dto/create-warehouse-yard.dto';
|
||||
import { UpdateWarehouseYardDto } from './dto/update-warehouse-yard.dto';
|
||||
@@ -44,6 +44,7 @@ export class WarehouseYardsService {
|
||||
// Ensure the parent warehouse exists.
|
||||
await this.warehousesService.findById(warehouseId);
|
||||
await this.assertCodeUnique(warehouseId, dto.code.trim());
|
||||
await this.assertCapacityWithinWarehouse(warehouseId, dto.capacityWeight ?? null, dto.capacityContainers ?? null);
|
||||
|
||||
return this.yardsRepository.create({
|
||||
warehouseId,
|
||||
@@ -69,14 +70,22 @@ export class WarehouseYardsService {
|
||||
await this.assertCodeUnique(existing.warehouseId, dto.code.trim(), id);
|
||||
}
|
||||
|
||||
const newCapacityWeight = dto.capacityWeight ?? existing.capacityWeight ?? null;
|
||||
const newCapacityContainers = dto.capacityContainers ?? existing.capacityContainers ?? null;
|
||||
|
||||
// Validate updated capacity doesn't exceed warehouse limits
|
||||
if (newCapacityWeight !== (existing.capacityWeight ?? null) || newCapacityContainers !== (existing.capacityContainers ?? null)) {
|
||||
await this.assertCapacityWithinWarehouse(existing.warehouseId, newCapacityWeight, newCapacityContainers, id);
|
||||
}
|
||||
|
||||
const status = dto.status ?? existing.status;
|
||||
|
||||
const updated = await this.yardsRepository.update(id, {
|
||||
name: dto.name?.trim() ?? existing.name,
|
||||
code: dto.code?.trim() ?? existing.code,
|
||||
type: dto.type ?? existing.type,
|
||||
capacityWeight: dto.capacityWeight ?? existing.capacityWeight,
|
||||
capacityContainers: dto.capacityContainers ?? existing.capacityContainers,
|
||||
capacityWeight: newCapacityWeight,
|
||||
capacityContainers: newCapacityContainers,
|
||||
maxWeight: dto.maxWeight ?? existing.maxWeight,
|
||||
maxVolume: dto.maxVolume ?? existing.maxVolume,
|
||||
status,
|
||||
@@ -97,4 +106,39 @@ export class WarehouseYardsService {
|
||||
throw new ConflictException(`Yard code ${code} already exists in this warehouse`);
|
||||
}
|
||||
}
|
||||
|
||||
private async assertCapacityWithinWarehouse(
|
||||
warehouseId: string,
|
||||
newCapacityWeight: number | null,
|
||||
newCapacityContainers: number | null,
|
||||
excludeYardId?: string,
|
||||
): Promise<void> {
|
||||
const warehouse = await this.warehousesService.findById(warehouseId);
|
||||
const yards = await this.findByWarehouse(warehouseId);
|
||||
|
||||
// Sum existing yard capacities, excluding the yard being updated if provided
|
||||
const otherYards = excludeYardId ? yards.filter((y) => y.id !== excludeYardId) : yards;
|
||||
const totalExistingWeight = otherYards.reduce((sum, y) => sum + (y.capacityWeight ?? 0), 0);
|
||||
const totalExistingContainers = otherYards.reduce((sum, y) => sum + (y.capacityContainers ?? 0), 0);
|
||||
|
||||
// Check weight capacity
|
||||
if (newCapacityWeight !== null && warehouse.capacityWeight != null) {
|
||||
const totalWeight = totalExistingWeight + newCapacityWeight;
|
||||
if (totalWeight > warehouse.capacityWeight) {
|
||||
throw new BadRequestException(
|
||||
`Total yard weight capacity (${totalWeight}t) exceeds warehouse limit (${warehouse.capacityWeight}t)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check container capacity
|
||||
if (newCapacityContainers !== null && warehouse.capacityContainers != null) {
|
||||
const totalContainers = totalExistingContainers + newCapacityContainers;
|
||||
if (totalContainers > warehouse.capacityContainers) {
|
||||
throw new BadRequestException(
|
||||
`Total yard container capacity (${totalContainers}) exceeds warehouse limit (${warehouse.capacityContainers})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { CreateWarehouseZoneDto } from './dto/create-warehouse-zone.dto';
|
||||
import { UpdateWarehouseZoneDto } from './dto/update-warehouse-zone.dto';
|
||||
@@ -43,6 +43,7 @@ export class WarehouseZonesService {
|
||||
// Ensure the parent yard exists.
|
||||
await this.yardsService.findById(yardId);
|
||||
await this.assertCodeUnique(yardId, dto.code.trim());
|
||||
await this.assertCapacityWithinYard(yardId, dto.capacityWeight ?? null, dto.capacityContainers ?? null);
|
||||
|
||||
return this.zonesRepository.create({
|
||||
yardId,
|
||||
@@ -68,14 +69,22 @@ export class WarehouseZonesService {
|
||||
await this.assertCodeUnique(existing.yardId, dto.code.trim(), id);
|
||||
}
|
||||
|
||||
const newCapacityWeight = dto.capacityWeight ?? existing.capacityWeight ?? null;
|
||||
const newCapacityContainers = dto.capacityContainers ?? existing.capacityContainers ?? null;
|
||||
|
||||
// Validate updated capacity doesn't exceed yard limits
|
||||
if (newCapacityWeight !== (existing.capacityWeight ?? null) || newCapacityContainers !== (existing.capacityContainers ?? null)) {
|
||||
await this.assertCapacityWithinYard(existing.yardId, newCapacityWeight, newCapacityContainers, id);
|
||||
}
|
||||
|
||||
const status = dto.status ?? existing.status;
|
||||
|
||||
const updated = await this.zonesRepository.update(id, {
|
||||
name: dto.name?.trim() ?? existing.name,
|
||||
code: dto.code?.trim() ?? existing.code,
|
||||
type: dto.type ?? existing.type,
|
||||
capacityWeight: dto.capacityWeight ?? existing.capacityWeight,
|
||||
capacityContainers: dto.capacityContainers ?? existing.capacityContainers,
|
||||
capacityWeight: newCapacityWeight,
|
||||
capacityContainers: newCapacityContainers,
|
||||
maxWeight: dto.maxWeight ?? existing.maxWeight,
|
||||
maxVolume: dto.maxVolume ?? existing.maxVolume,
|
||||
status,
|
||||
@@ -96,4 +105,39 @@ export class WarehouseZonesService {
|
||||
throw new ConflictException(`Zone code ${code} already exists in this yard`);
|
||||
}
|
||||
}
|
||||
|
||||
private async assertCapacityWithinYard(
|
||||
yardId: string,
|
||||
newCapacityWeight: number | null,
|
||||
newCapacityContainers: number | null,
|
||||
excludeZoneId?: string,
|
||||
): Promise<void> {
|
||||
const yard = await this.yardsService.findById(yardId);
|
||||
const zones = await this.findByYard(yardId);
|
||||
|
||||
// Sum existing zone capacities, excluding the zone being updated if provided
|
||||
const otherZones = excludeZoneId ? zones.filter((z) => z.id !== excludeZoneId) : zones;
|
||||
const totalExistingWeight = otherZones.reduce((sum, z) => sum + (z.capacityWeight ?? 0), 0);
|
||||
const totalExistingContainers = otherZones.reduce((sum, z) => sum + (z.capacityContainers ?? 0), 0);
|
||||
|
||||
// Check weight capacity
|
||||
if (newCapacityWeight !== null && yard.capacityWeight != null) {
|
||||
const totalWeight = totalExistingWeight + newCapacityWeight;
|
||||
if (totalWeight > yard.capacityWeight) {
|
||||
throw new BadRequestException(
|
||||
`Total zone weight capacity (${totalWeight}t) exceeds yard limit (${yard.capacityWeight}t)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check container capacity
|
||||
if (newCapacityContainers !== null && yard.capacityContainers != null) {
|
||||
const totalContainers = totalExistingContainers + newCapacityContainers;
|
||||
if (totalContainers > yard.capacityContainers) {
|
||||
throw new BadRequestException(
|
||||
`Total zone container capacity (${totalContainers}) exceeds yard limit (${yard.capacityContainers})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,41 +808,41 @@ export const POSITION_PERMISSION_PRESETS = {
|
||||
// permission catalog (all CRUD across bookings, contracts, scheduling,
|
||||
// fleet, warehouse, mile, finance, settings, staff).
|
||||
operationsChief: dedupe([...BOOKING_RULE_ENGINE_PERMISSION_KEYS]),
|
||||
// Dispatcher: warehouse floor operations — receive/GRN, move, load/unload,
|
||||
// inspect, dispatch, gate, release/deliver, interchange docs, fee invoices,
|
||||
// plus truck dispatch on the mile legs and read-only operational context.
|
||||
// Allocation & fee rules are VIEW-ONLY — never create/update/delete.
|
||||
// Dispatcher: full CRUD on warehouse management (incl. import/export/intercity
|
||||
// inventory flows) and fleet management, plus truck dispatch on the mile legs
|
||||
// and operational context. The ONE carve-out: allocation & fee rules stay
|
||||
// VIEW-ONLY — a dispatcher never creates/updates/deletes those rules.
|
||||
dispatcher: dedupe([
|
||||
// Warehouse management — full CRUD.
|
||||
FREIGHT_PERMS.warehouseDashboard.view,
|
||||
FREIGHT_PERMS.warehouses.view,
|
||||
FREIGHT_PERMS.warehouseYards.view,
|
||||
FREIGHT_PERMS.warehouseZones.view,
|
||||
FREIGHT_PERMS.warehouseInventory.view,
|
||||
FREIGHT_PERMS.warehouseInventory.receive,
|
||||
FREIGHT_PERMS.warehouseInventory.move,
|
||||
FREIGHT_PERMS.warehouseInventory.load,
|
||||
FREIGHT_PERMS.warehouseInventory.unload,
|
||||
FREIGHT_PERMS.warehouseInventory.dispatch,
|
||||
FREIGHT_PERMS.warehouseInventory.gatePass,
|
||||
FREIGHT_PERMS.warehouseInventory.release,
|
||||
FREIGHT_PERMS.warehouseInventory.deliver,
|
||||
FREIGHT_PERMS.warehouseInventory.inspect,
|
||||
FREIGHT_PERMS.warehouseInspectionReports.view,
|
||||
FREIGHT_PERMS.warehouseInspectionReports.create,
|
||||
FREIGHT_PERMS.warehouseInspectionReports.update,
|
||||
FREIGHT_PERMS.interchangeDocuments.view,
|
||||
FREIGHT_PERMS.interchangeDocuments.generate,
|
||||
FREIGHT_PERMS.interchangeDocuments.acknowledge,
|
||||
FREIGHT_PERMS.warehouseFeeInvoices.view,
|
||||
FREIGHT_PERMS.warehouseFeeInvoices.generate,
|
||||
...Object.values(FREIGHT_PERMS.warehouses),
|
||||
...Object.values(FREIGHT_PERMS.warehouseYards),
|
||||
...Object.values(FREIGHT_PERMS.warehouseZones),
|
||||
...Object.values(FREIGHT_PERMS.warehouseInventory),
|
||||
...Object.values(FREIGHT_PERMS.warehouseInspectionReports),
|
||||
...Object.values(FREIGHT_PERMS.interchangeDocuments),
|
||||
...Object.values(FREIGHT_PERMS.warehouseFeeInvoices),
|
||||
// View-only on the rules that govern allocation and fees.
|
||||
FREIGHT_PERMS.warehouseAllocationRules.view,
|
||||
FREIGHT_PERMS.warehouseFeeRules.view,
|
||||
// Fleet management — full CRUD.
|
||||
...Object.values(FREIGHT_PERMS.fleet),
|
||||
FREIGHT_PERMS.fleetDashboard.view,
|
||||
...Object.values(FREIGHT_PERMS.fleetReports),
|
||||
...Object.values(FREIGHT_PERMS.vehicles),
|
||||
...Object.values(FREIGHT_PERMS.drivers),
|
||||
...Object.values(FREIGHT_PERMS.tracking),
|
||||
...Object.values(FREIGHT_PERMS.fuel),
|
||||
...Object.values(FREIGHT_PERMS.maintenance),
|
||||
...Object.values(FREIGHT_PERMS.locomotives),
|
||||
...Object.values(FREIGHT_PERMS.wagons),
|
||||
...Object.values(FREIGHT_PERMS.trains),
|
||||
...Object.values(FREIGHT_PERMS.routes),
|
||||
...Object.values(FREIGHT_PERMS.containers),
|
||||
...Object.values(FREIGHT_PERMS.cargoes),
|
||||
// Truck dispatch on the EDR mile legs + operational context.
|
||||
FREIGHT_PERMS.firstMile.view,
|
||||
FREIGHT_PERMS.firstMile.assignVehicles,
|
||||
FREIGHT_PERMS.lastMile.view,
|
||||
FREIGHT_PERMS.lastMile.assignVehicles,
|
||||
...Object.values(FREIGHT_PERMS.firstMile),
|
||||
...Object.values(FREIGHT_PERMS.lastMile),
|
||||
FREIGHT_PERMS.trainScheduling.view,
|
||||
FREIGHT_PERMS.bookings.operations,
|
||||
]),
|
||||
|
||||
Reference in New Issue
Block a user