mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 12:58:13 +00:00
Warehouses confilct fix
This commit is contained in:
@@ -28,10 +28,6 @@ export class Warehouse extends BaseEntity {
|
||||
@Column({ name: 'station_id', type: 'uuid', nullable: true })
|
||||
stationId?: string | null;
|
||||
|
||||
@ManyToOne(() => Yard, { nullable: true })
|
||||
@JoinColumn({ name: 'station_id' })
|
||||
facility?: Yard | null;
|
||||
|
||||
@Column({ name: 'location_name', type: 'varchar', length: 200, nullable: true })
|
||||
locationName?: string | null;
|
||||
|
||||
|
||||
@@ -142,16 +142,4 @@ export class WarehouseInventoryController {
|
||||
dispatch(@Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy?: string) {
|
||||
return this.inventoryService.dispatch(id, performedBy);
|
||||
}
|
||||
|
||||
@Post(':id/load')
|
||||
@ApiOperation({ summary: 'Mark ready inventory as loaded' })
|
||||
load(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.inventoryService.load(id);
|
||||
}
|
||||
|
||||
@Post(':id/dispatch')
|
||||
@ApiOperation({ summary: 'Dispatch loaded inventory' })
|
||||
dispatch(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.inventoryService.dispatch(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { Between, DataSource, EntityManager, FindManyOptions, ILike, LessThanOrE
|
||||
|
||||
import { FilterWarehouseInventoryDto } from './dto/filter-inventory.dto';
|
||||
import { InquiryWarehouseInventoryDto } from './dto/inquiry-inventory.dto';
|
||||
import { LoadInventoryDto } from './dto/load-inventory.dto';
|
||||
import { MoveInventoryDto } from './dto/move-inventory.dto';
|
||||
import { ReceiveWarehouseInventoryDto } from './dto/receive-inventory.dto';
|
||||
import { ReserveInventoryDto } from './dto/reserve-inventory.dto';
|
||||
import { UnloadBookingDto } from './dto/unload-booking.dto';
|
||||
@@ -46,6 +48,103 @@ export interface InventoryInquiryResult {
|
||||
readyForLoadingAt: Date | null;
|
||||
}
|
||||
|
||||
interface BookingSummaryRow {
|
||||
id: string;
|
||||
reference: string | null;
|
||||
status: string | null;
|
||||
customer: string | null;
|
||||
}
|
||||
|
||||
interface ArrivalQueueRow {
|
||||
bookingId: string;
|
||||
bookingReference: string | null;
|
||||
customer: string | null;
|
||||
cargo: string | null;
|
||||
container: string | null;
|
||||
arrivalDate: Date | null;
|
||||
bookingStatus: string | null;
|
||||
inventoryId: string | null;
|
||||
currentStatus: string | null;
|
||||
inspectionStatus: string | null;
|
||||
facility: string | null;
|
||||
warehouse: string | null;
|
||||
yard: string | null;
|
||||
zone: string | null;
|
||||
}
|
||||
|
||||
export interface ArrivalQueueItem {
|
||||
bookingId: string;
|
||||
bookingReference: string | null;
|
||||
customer: string | null;
|
||||
cargo: string | null;
|
||||
container: string | null;
|
||||
facility: string | null;
|
||||
warehouse: string | null;
|
||||
yard: string | null;
|
||||
zone: string | null;
|
||||
inventoryId: string | null;
|
||||
currentStatus: string | null;
|
||||
arrivalDate: Date | null;
|
||||
inspectionStatus: string | null;
|
||||
unloaded: boolean;
|
||||
}
|
||||
|
||||
interface DefaultLocation {
|
||||
warehouseId: string;
|
||||
facilityId?: string | null;
|
||||
yardId: string;
|
||||
zoneId: string;
|
||||
}
|
||||
|
||||
export interface AutoUnloadResult {
|
||||
processedCount: number;
|
||||
skippedCount: number;
|
||||
failedCount: number;
|
||||
results: Array<{
|
||||
bookingId: string;
|
||||
inventoryId?: string;
|
||||
status: 'PROCESSED' | 'FAILED';
|
||||
reason?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AutoLoadResult {
|
||||
loadedCount: number;
|
||||
skippedCount: number;
|
||||
results: Array<{
|
||||
inventoryId: string;
|
||||
status: 'LOADED' | 'SKIPPED';
|
||||
reason?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface WarehouseDashboardSummary {
|
||||
totalWarehouses: number;
|
||||
totalInventory: number;
|
||||
receivedToday: number;
|
||||
stored: number;
|
||||
reserved: number;
|
||||
readyForLoading: number;
|
||||
loaded: number;
|
||||
dispatched: number;
|
||||
}
|
||||
|
||||
interface LocationRef {
|
||||
warehouseId: string;
|
||||
yardId: string;
|
||||
zoneId: string;
|
||||
}
|
||||
|
||||
interface LocationNode {
|
||||
capacityWeight?: number | null;
|
||||
capacityContainers?: number | null;
|
||||
currentWeight: number;
|
||||
maxWeight?: number | null;
|
||||
maxVolume?: number | null;
|
||||
currentVolume?: number | null;
|
||||
currentContainers: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WarehouseInventoryService {
|
||||
constructor(
|
||||
@@ -386,7 +485,7 @@ export class WarehouseInventoryService {
|
||||
}),
|
||||
);
|
||||
|
||||
await this.applyCapacityDelta(manager, dto.warehouseId, dto.yardId, dto.zoneId, weight, volume, containerCount, +1);
|
||||
await this.applyCapacityDelta(manager, dto, weight, volume, containerCount);
|
||||
|
||||
await this.activityLog.record(
|
||||
{
|
||||
@@ -405,7 +504,7 @@ export class WarehouseInventoryService {
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
async move(id: string, dto: MoveWarehouseInventoryDto): Promise<WarehouseInventory> {
|
||||
async move(id: string, dto: MoveInventoryDto): Promise<WarehouseInventory> {
|
||||
const movedId = await this.dataSource.transaction(async (manager) => {
|
||||
const item = await manager.getRepository(WarehouseInventory).findOne({
|
||||
where: { id },
|
||||
@@ -428,12 +527,12 @@ export class WarehouseInventoryService {
|
||||
const containerCount = item.containerId ? Math.round(Number(item.quantity) || 0) : 0;
|
||||
|
||||
if (item.warehouseId !== dto.warehouseId) {
|
||||
this.assertCapacity('Warehouse', warehouse, weight, containerCount);
|
||||
this.assertCapacity('Warehouse', warehouse, weight, Number(item.volume) || 0, containerCount);
|
||||
}
|
||||
if (item.yardId !== dto.yardId) {
|
||||
this.assertCapacity('Yard', yard, weight, containerCount);
|
||||
this.assertCapacity('Yard', yard, weight, Number(item.volume) || 0, containerCount);
|
||||
}
|
||||
this.assertCapacity('Zone', zone, weight, containerCount);
|
||||
this.assertCapacity('Zone', zone, weight, Number(item.volume) || 0, containerCount);
|
||||
|
||||
await this.applyCapacityDelta(
|
||||
manager,
|
||||
@@ -443,10 +542,11 @@ export class WarehouseInventoryService {
|
||||
zoneId: item.zoneId,
|
||||
},
|
||||
-weight,
|
||||
-(Number(item.volume) || 0),
|
||||
-containerCount,
|
||||
);
|
||||
|
||||
await this.applyCapacityDelta(manager, dto, weight, containerCount);
|
||||
await this.applyCapacityDelta(manager, dto, weight, Number(item.volume) || 0, containerCount);
|
||||
|
||||
item.warehouseId = dto.warehouseId;
|
||||
item.yardId = dto.yardId;
|
||||
@@ -597,34 +697,6 @@ export class WarehouseInventoryService {
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
async store(id: string): Promise<WarehouseInventory> {
|
||||
const item = await this.findById(id);
|
||||
|
||||
if (item.status !== 'RECEIVED' && item.status !== 'ARRIVED_AT_WAREHOUSE') {
|
||||
throw new BadRequestException(`Only RECEIVED inventory can be stored (current: ${item.status})`);
|
||||
}
|
||||
|
||||
await this.inventoryRepository.update(id, { status: 'STORED' });
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
async reserve(id: string, dto: { bookingId?: string }): Promise<WarehouseInventory> {
|
||||
const item = await this.findById(id);
|
||||
|
||||
if (item.status !== 'STORED') {
|
||||
throw new BadRequestException('Only STORED inventory can be reserved.');
|
||||
}
|
||||
|
||||
const bookingId = dto.bookingId ?? item.bookingId;
|
||||
await this.assertPaidBooking(this.dataSource.manager, bookingId);
|
||||
|
||||
await this.inventoryRepository.update(id, {
|
||||
bookingId,
|
||||
status: 'RESERVED',
|
||||
});
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
// ── Loading records (Batch 3) ─────────────────────────────────────────────
|
||||
|
||||
async findLoadings(
|
||||
@@ -663,44 +735,13 @@ export class WarehouseInventoryService {
|
||||
});
|
||||
}
|
||||
|
||||
async dispatch(id: string, performedBy?: string): Promise<WarehouseInventory> {
|
||||
const item = await this.findById(id);
|
||||
|
||||
if (item.status !== 'UNDER_INSPECTION') {
|
||||
throw new BadRequestException(
|
||||
`Only items in UNDER_INSPECTION can be marked READY_FOR_LOADING (current: ${item.status})`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.inventoryRepository.update(id, {
|
||||
status: 'READY_FOR_LOADING',
|
||||
readyForLoadingAt: new Date(),
|
||||
dispatch(id: string, performedBy?: string): Promise<WarehouseInventory> {
|
||||
return this.transition(id, 'DISPATCHED', {
|
||||
timestampField: 'dispatchedAt',
|
||||
activityType: 'INVENTORY_DISPATCHED',
|
||||
description: 'Inventory dispatched',
|
||||
performedBy,
|
||||
});
|
||||
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
async load(id: string): Promise<WarehouseInventory> {
|
||||
const item = await this.findById(id);
|
||||
|
||||
if (item.status !== 'READY_FOR_LOADING') {
|
||||
throw new BadRequestException(`Only READY_FOR_LOADING inventory can be loaded (current: ${item.status})`);
|
||||
}
|
||||
|
||||
await this.assertPaidBooking(this.dataSource.manager, item.bookingId);
|
||||
await this.inventoryRepository.update(id, { status: 'LOADED' });
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
async dispatch(id: string): Promise<WarehouseInventory> {
|
||||
const item = await this.findById(id);
|
||||
|
||||
if (item.status !== 'LOADED') {
|
||||
throw new BadRequestException(`Only LOADED inventory can be dispatched (current: ${item.status})`);
|
||||
}
|
||||
|
||||
await this.inventoryRepository.update(id, { status: 'DISPATCHED' });
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
async dashboardSummary(filter: FilterWarehouseInventoryDto): Promise<WarehouseDashboardSummary> {
|
||||
@@ -858,7 +899,7 @@ export class WarehouseInventoryService {
|
||||
|
||||
private async validateLocation(
|
||||
manager: EntityManager,
|
||||
dto: ReceiveWarehouseInventoryDto,
|
||||
dto: LocationRef,
|
||||
): Promise<{ warehouse: Warehouse; yard: WarehouseYard; zone: WarehouseZone }> {
|
||||
const warehouse = await manager.getRepository(Warehouse).findOne({ where: { id: dto.warehouseId } });
|
||||
if (!warehouse) throw new NotFoundException(`Warehouse ${dto.warehouseId} not found`);
|
||||
@@ -879,6 +920,38 @@ export class WarehouseInventoryService {
|
||||
}
|
||||
}
|
||||
|
||||
private async getBookingStatus(bookingId: string): Promise<string | null> {
|
||||
const [row]: Array<{ status: string | null }> = await this.dataSource.query(
|
||||
'SELECT status FROM freight.bookings WHERE id = $1 AND deleted_at IS NULL LIMIT 1',
|
||||
[bookingId],
|
||||
);
|
||||
return row?.status ?? null;
|
||||
}
|
||||
|
||||
private async attachBookingSummaries(items: WarehouseInventory[]): Promise<void> {
|
||||
const bookingIds = [...new Set(items.map((item) => item.bookingId).filter(Boolean))] as string[];
|
||||
if (bookingIds.length === 0) return;
|
||||
|
||||
const rows: BookingSummaryRow[] = await this.dataSource.query(
|
||||
`SELECT b.id, b.reference, b.status, company.name AS customer
|
||||
FROM freight.bookings b
|
||||
LEFT JOIN freight.companies company ON company.id = b.company_id
|
||||
WHERE b.id = ANY($1) AND b.deleted_at IS NULL`,
|
||||
[bookingIds],
|
||||
);
|
||||
const summaries = new Map(rows.map((row) => [row.id, row]));
|
||||
|
||||
items.forEach((item) => {
|
||||
const summary = item.bookingId ? summaries.get(item.bookingId) : undefined;
|
||||
if (!summary) return;
|
||||
Object.assign(item, {
|
||||
bookingReference: summary.reference,
|
||||
bookingStatus: summary.status,
|
||||
customerName: summary.customer,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private assertCapacity(
|
||||
label: string,
|
||||
node: LocationNode,
|
||||
@@ -909,21 +982,24 @@ export class WarehouseInventoryService {
|
||||
|
||||
private async applyCapacityDelta(
|
||||
manager: EntityManager,
|
||||
dto: ReceiveWarehouseInventoryDto,
|
||||
location: LocationRef,
|
||||
weightAdd: number,
|
||||
volumeAdd: number,
|
||||
containerAdd: number,
|
||||
): Promise<void> {
|
||||
const apply = sign === 1 ? manager.increment.bind(manager) : manager.decrement.bind(manager);
|
||||
const targets: Array<[typeof Warehouse | typeof WarehouseYard | typeof WarehouseZone, string]> = [
|
||||
[Warehouse, warehouseId],
|
||||
[WarehouseYard, yardId],
|
||||
[WarehouseZone, zoneId],
|
||||
[Warehouse, location.warehouseId],
|
||||
[WarehouseYard, location.yardId],
|
||||
[WarehouseZone, location.zoneId],
|
||||
];
|
||||
|
||||
for (const [entity, id] of targets) {
|
||||
if (weight) await apply(entity, { id }, 'currentWeight', weight);
|
||||
if (volume) await apply(entity, { id }, 'currentVolume', volume);
|
||||
if (containers) await apply(entity, { id }, 'currentContainers', containers);
|
||||
if (weightAdd > 0) await manager.increment(entity, { id }, 'currentWeight', weightAdd);
|
||||
if (weightAdd < 0) await manager.decrement(entity, { id }, 'currentWeight', Math.abs(weightAdd));
|
||||
if (volumeAdd > 0) await manager.increment(entity, { id }, 'currentVolume', volumeAdd);
|
||||
if (volumeAdd < 0) await manager.decrement(entity, { id }, 'currentVolume', Math.abs(volumeAdd));
|
||||
if (containerAdd > 0) await manager.increment(entity, { id }, 'currentContainers', containerAdd);
|
||||
if (containerAdd < 0) await manager.decrement(entity, { id }, 'currentContainers', Math.abs(containerAdd));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user