mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 05:58:18 +00:00
feat(warehouses): allow deleting a warehouse
Soft-delete route guarded by warehouses:delete, refused with 409 while yards remain — zones and inventory hang off a yard, so cascading would orphan stock. Backoffice list gets a delete action in both views, omitted when the user lacks the permission.
This commit is contained in:
@@ -50,6 +50,10 @@ 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 {
|
||||
BulkRegisterBacklogDto,
|
||||
RegisterBacklogContainerDto,
|
||||
} from './dto/register-backlog.dto';
|
||||
import { ReleaseOrderDto } from './dto/release-order.dto';
|
||||
import { ReserveInventoryDto } from './dto/reserve-inventory.dto';
|
||||
import { UnloadBookingDto } from './dto/unload-booking.dto';
|
||||
@@ -2863,6 +2867,136 @@ export class WarehouseInventoryService {
|
||||
await this.lastMileService.acceptBooking(booking.reference);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a loaded container that is already physically in a yard but was
|
||||
* never entered in the system. Unlike receive(), there is no booking, no
|
||||
* truck entrance to record (nobody remembers the driver of a box that has sat
|
||||
* for months) and the arrival is backdated to when it actually turned up.
|
||||
*
|
||||
* The row is flagged `backlogRegistration`, which keeps the fee engine off it
|
||||
* entirely — see WarehouseFeeService.previewForInventory. Capacity is still
|
||||
* charged, because the box does occupy the yard.
|
||||
*/
|
||||
async registerBacklogContainer(dto: RegisterBacklogContainerDto): Promise<WarehouseInventory> {
|
||||
const id = await this.dataSource.transaction((manager) => this.saveBacklogContainer(manager, dto));
|
||||
const saved = await this.inventoryRepository.findById(id);
|
||||
if (!saved) throw new NotFoundException(`Inventory ${id} not found after registration`);
|
||||
return saved;
|
||||
}
|
||||
|
||||
/** The write itself, so single and bulk share one transaction each. */
|
||||
private async saveBacklogContainer(
|
||||
manager: EntityManager,
|
||||
dto: RegisterBacklogContainerDto,
|
||||
): Promise<string> {
|
||||
const containerNumber = dto.containerNumber.trim().toUpperCase();
|
||||
const arrivedAt = new Date(dto.arrivedAt);
|
||||
if (Number.isNaN(arrivedAt.getTime())) {
|
||||
throw new BadRequestException(`Arrival date "${dto.arrivedAt}" is not a valid date`);
|
||||
}
|
||||
if (arrivedAt.getTime() > Date.now()) {
|
||||
throw new BadRequestException('Arrival date cannot be in the future');
|
||||
}
|
||||
|
||||
{
|
||||
const { warehouse, yard, zone } = await this.validateLocation(manager, dto);
|
||||
|
||||
const containerType = await manager.query(
|
||||
`SELECT id FROM freight.container_types WHERE id = $1 AND deleted_at IS NULL`,
|
||||
[dto.containerTypeId],
|
||||
);
|
||||
if (containerType.length === 0) {
|
||||
throw new NotFoundException(`Container type ${dto.containerTypeId} not found`);
|
||||
}
|
||||
|
||||
// container_number is UNIQUE — reuse the existing record rather than
|
||||
// colliding, so a box seen before keeps one identity.
|
||||
const containers = manager.getRepository(Container);
|
||||
let container = await containers.findOne({ where: { containerNumber } });
|
||||
if (container) {
|
||||
const alreadyHeld = await manager.getRepository(WarehouseInventory).findOne({
|
||||
where: { containerId: container.id, status: In(['RECEIVED', 'STORED', 'READY_FOR_PICKUP']) },
|
||||
});
|
||||
if (alreadyHeld) {
|
||||
throw new BadRequestException(
|
||||
`Container ${containerNumber} is already in the warehouse (status ${alreadyHeld.status})`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
container = await containers.save(
|
||||
containers.create({
|
||||
containerNumber,
|
||||
containerTypeId: dto.containerTypeId,
|
||||
sealNumber: dto.sealNumber?.trim() || null,
|
||||
tareWeight: dto.tareWeight ?? 0,
|
||||
maxGrossWeight: dto.maxGrossWeight ?? 0,
|
||||
status: 'LOADED',
|
||||
bookingId: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const weight = Number(dto.weight) || 0;
|
||||
const volume = Number(dto.volume) || 0;
|
||||
this.assertCapacity('Warehouse', warehouse, weight, volume, 1);
|
||||
this.assertCapacity('Yard', yard, weight, volume, 1);
|
||||
this.assertCapacity('Zone', zone, weight, volume, 1);
|
||||
|
||||
const owner = dto.companyName?.trim() || null;
|
||||
const grnNumber = this.generateGrnNumber('WH', 'BACKLOG', arrivedAt, owner);
|
||||
const saved = await manager.getRepository(WarehouseInventory).save(
|
||||
manager.getRepository(WarehouseInventory).create({
|
||||
warehouseId: dto.warehouseId,
|
||||
yardId: dto.yardId,
|
||||
zoneId: dto.zoneId,
|
||||
bookingId: null,
|
||||
containerId: container.id,
|
||||
companyId: dto.companyId ?? null,
|
||||
companyName: owner,
|
||||
quantity: 1,
|
||||
weight,
|
||||
volume: dto.volume ?? null,
|
||||
grnNumber,
|
||||
status: 'RECEIVED',
|
||||
arrivedAt,
|
||||
backlogRegistration: true,
|
||||
notes: this.buildReceiveNote({
|
||||
grnNumber,
|
||||
notes:
|
||||
dto.notes?.trim() ||
|
||||
`Backlog registration — already in yard, arrived ${arrivedAt.toISOString().slice(0, 10)}`,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await this.applyCapacityDelta(manager, dto, weight, volume, 1);
|
||||
return saved.id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk backlog registration. All-or-nothing: one bad row rejects the sheet,
|
||||
* so a half-registered yard can never happen.
|
||||
*/
|
||||
async bulkRegisterBacklogContainers(dto: BulkRegisterBacklogDto): Promise<WarehouseInventory[]> {
|
||||
const numbers = dto.containers.map((c) => c.containerNumber.trim().toUpperCase());
|
||||
const seen = new Set<string>();
|
||||
const repeated = [...new Set(numbers.filter((n) => (seen.has(n) ? true : (seen.add(n), false))))];
|
||||
if (repeated.length > 0) {
|
||||
throw new BadRequestException(`Container number(s) repeated in the upload: ${repeated.join(', ')}`);
|
||||
}
|
||||
|
||||
const ids = await this.dataSource.transaction(async (manager) => {
|
||||
const written: string[] = [];
|
||||
for (const container of dto.containers) {
|
||||
written.push(await this.saveBacklogContainer(manager, container));
|
||||
}
|
||||
return written;
|
||||
});
|
||||
|
||||
return this.inventoryRepository.findAll({ where: { id: In(ids) } });
|
||||
}
|
||||
|
||||
async receive(dto: ReceiveWarehouseInventoryDto): Promise<WarehouseInventory> {
|
||||
const bookingDirection = dto.bookingId ? await this.getBookingDirection(dto.bookingId) : null;
|
||||
await this.assertExportBookingPaid(dto.bookingId, bookingDirection);
|
||||
|
||||
Reference in New Issue
Block a user