Files
edr-platform/apps/edr-freight-api/src/modules/warehouses/warehouses.service.ts
Hagernesh b7a831b063 feat(warehouse): route-based receive direction + Export Receive Queue (Batch 2)
- Direction derived from route via existing deriveTradeDirection (eligible-bookings,
  bulk receive guard, getBookingDirection) instead of stored trade_direction
- Export tab sub-tabs (Receive Queue + Ready-to-Load/Loaded/Dispatch placeholders)
- Receive Queue: full column set + per-row Receive; eligible-bookings adds customerId
- create/update warehouse: map DB errors to 400; dashboard: distinct per-status colors

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 10:26:50 +00:00

134 lines
4.6 KiB
TypeScript

import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { FindManyOptions, ILike, QueryFailedError } from 'typeorm';
import { CreateWarehouseDto } from './dto/create-warehouse.dto';
import { FilterWarehouseDto } from './dto/filter-warehouse.dto';
import { UpdateWarehouseDto } from './dto/update-warehouse.dto';
import { Warehouse } from './entities/warehouse.entity';
import { WarehousesRepository } from './warehouses.repository';
@Injectable()
export class WarehousesService {
constructor(private readonly warehousesRepository: WarehousesRepository) {}
async findAll(filter: FilterWarehouseDto): Promise<Warehouse[]> {
const where: FindManyOptions<Warehouse>['where'] = {
...(filter.type ? { type: filter.type } : {}),
...(filter.stationId ? { stationId: filter.stationId } : {}),
...(filter.status ? { status: filter.status } : {}),
};
const search = filter.search?.trim();
const whereClauses = search
? [
{ ...where, name: ILike(`%${search}%`) },
{ ...where, code: ILike(`%${search}%`) },
{ ...where, locationName: ILike(`%${search}%`) },
]
: where;
return this.warehousesRepository.findAll({
where: whereClauses,
relations: { facility: true },
order: { code: 'ASC' },
});
}
async findById(id: string): Promise<Warehouse> {
const warehouse = await this.warehousesRepository.findById(id, {
relations: { facility: true, yards: { zones: true } },
});
if (!warehouse) {
throw new NotFoundException(`Warehouse ${id} not found`);
}
return warehouse;
}
async create(dto: CreateWarehouseDto): Promise<Warehouse> {
await this.assertCodeUnique(dto.code.trim());
try {
return await this.warehousesRepository.create({
name: dto.name.trim(),
code: dto.code.trim(),
type: dto.type,
stationId: dto.stationId ?? null,
facilityId: dto.facilityId ?? null,
locationName: dto.locationName?.trim() ?? null,
capacityWeight: dto.capacityWeight ?? null,
capacityContainers: dto.capacityContainers ?? null,
maxWeight: dto.maxWeight ?? dto.capacityWeight ?? null,
maxVolume: dto.maxVolume ?? null,
currentWeight: 0,
currentContainers: 0,
currentVolume: 0,
status: 'ACTIVE',
isActive: true,
});
} catch (error) {
this.mapDbError(error);
}
}
async update(id: string, dto: UpdateWarehouseDto): Promise<Warehouse> {
const existing = await this.findById(id);
if (dto.code && dto.code.trim() !== existing.code) {
await this.assertCodeUnique(dto.code.trim(), id);
}
const status = dto.status ?? existing.status;
let updated;
try {
updated = await this.warehousesRepository.update(id, {
name: dto.name?.trim() ?? existing.name,
code: dto.code?.trim() ?? existing.code,
type: dto.type ?? existing.type,
stationId: dto.stationId ?? existing.stationId,
facilityId: dto.facilityId ?? existing.facilityId,
locationName: dto.locationName?.trim() ?? existing.locationName,
capacityWeight: dto.capacityWeight ?? existing.capacityWeight,
capacityContainers: dto.capacityContainers ?? existing.capacityContainers,
maxWeight: dto.maxWeight ?? existing.maxWeight,
maxVolume: dto.maxVolume ?? existing.maxVolume,
status,
isActive: status === 'ACTIVE',
});
} catch (error) {
this.mapDbError(error);
}
if (!updated) {
throw new NotFoundException(`Warehouse ${id} not found`);
}
return this.findById(id);
}
/** Map low-level DB errors (FK / length / etc.) to a clean 400 instead of a 500. */
private mapDbError(error: unknown): never {
if (error instanceof QueryFailedError) {
const driver = (error as QueryFailedError & { driverError?: { code?: string; detail?: string } }).driverError;
if (driver?.code === '23503') {
throw new BadRequestException('Selected facility does not exist.');
}
if (driver?.code === '22001') {
throw new BadRequestException('A field is too long (code max 40, name max 160 characters).');
}
throw new BadRequestException(driver?.detail ?? error.message ?? 'Invalid warehouse data.');
}
throw error as Error;
}
private async assertCodeUnique(code: string, ignoreId?: string): Promise<void> {
const [existing] = await this.warehousesRepository.findAll({ where: { code } });
if (existing && existing.id !== ignoreId) {
throw new ConflictException(`Warehouse code ${code} already exists`);
}
}
}