Files
edr-platform/apps/edr-freight-api/src/modules/warehouses/delete-yard-zone-guards.spec.ts
Hagernesh a6ea1a48ac feat(warehouses): track physical container stack and slot positions
Extends the warehouse hierarchy below zone with ground stacks and vertical
slots, so a container's exact position is recorded rather than only its zone.

- freight.warehouse_zone_stacks / warehouse_zone_slots, plus nullable
  stack_id / slot_id on warehouse_inventory (existing rows stay valid)
- slot occupancy is derived from inventory status, guarded by a partial
  unique index, so no exit path has to remember to free a slot
- placement service: hierarchy validation, bottom-up stacking rules,
  accessibility/blocking-container reads, capacity vs slot summaries
- stack CRUD with auto-generated slots; reuses warehouse-zone permissions
- slot support folded into the existing move()/store() paths
- fix: validateLocation now rejects a mismatched warehouse/yard/zone triple
- seed:warehouse-layout builds the layout from a JSON config
2026-08-28 16:09:41 +00:00

89 lines
3.4 KiB
TypeScript

import { ConflictException, NotFoundException } from '@nestjs/common';
import { WarehouseYardsService } from './warehouse-yards.service';
import { WarehouseZonesService } from './warehouse-zones.service';
/**
* Soft-deleting a parent would leave its children pointing at a row every
* joining query drops, so both removes refuse while children exist.
*/
function makeYardsService(yard: unknown) {
const yardsRepository = {
findById: jest.fn().mockResolvedValue(yard),
softDelete: jest.fn().mockResolvedValue(undefined),
};
const service = Object.create(WarehouseYardsService.prototype) as Record<string, unknown>;
service.yardsRepository = yardsRepository;
return { service: service as unknown as WarehouseYardsService, yardsRepository };
}
function makeZonesService(zone: unknown, heldInventory: number, configuredStacks = 0) {
const zonesRepository = {
findById: jest.fn().mockResolvedValue(zone),
softDelete: jest.fn().mockResolvedValue(undefined),
};
const inventoryRepository = {
findAndCount: jest.fn().mockResolvedValue([[], heldInventory]),
};
const service = Object.create(WarehouseZonesService.prototype) as Record<string, unknown>;
service.zonesRepository = zonesRepository;
service.inventoryRepository = inventoryRepository;
service.dataSource = { query: jest.fn().mockResolvedValue([{ count: configuredStacks }]) };
return { service: service as unknown as WarehouseZonesService, zonesRepository };
}
describe('WarehouseYardsService.remove', () => {
it('soft-deletes a yard with no zones', async () => {
const { service, yardsRepository } = makeYardsService({ id: 'y1', code: 'CY-A', zones: [] });
await expect(service.remove('y1')).resolves.toEqual({ id: 'y1', deleted: true });
expect(yardsRepository.softDelete).toHaveBeenCalledWith('y1');
});
it('refuses while zones remain', async () => {
const { service, yardsRepository } = makeYardsService({
id: 'y1',
code: 'CY-A',
zones: [{ id: 'z1' }],
});
await expect(service.remove('y1')).rejects.toBeInstanceOf(ConflictException);
expect(yardsRepository.softDelete).not.toHaveBeenCalled();
});
it('404s on an unknown yard', async () => {
const { service } = makeYardsService(null);
await expect(service.remove('nope')).rejects.toBeInstanceOf(NotFoundException);
});
});
describe('WarehouseZonesService.remove', () => {
it('soft-deletes an empty zone', async () => {
const { service, zonesRepository } = makeZonesService({ id: 'z1', code: 'ZA' }, 0);
await expect(service.remove('z1')).resolves.toEqual({ id: 'z1', deleted: true });
expect(zonesRepository.softDelete).toHaveBeenCalledWith('z1');
});
it('refuses while inventory sits in it', async () => {
const { service, zonesRepository } = makeZonesService({ id: 'z1', code: 'ZA' }, 16);
await expect(service.remove('z1')).rejects.toBeInstanceOf(ConflictException);
expect(zonesRepository.softDelete).not.toHaveBeenCalled();
});
it('refuses while ground stacks are still configured in it', async () => {
const { service, zonesRepository } = makeZonesService({ id: 'z1', code: 'ZA' }, 0, 20);
await expect(service.remove('z1')).rejects.toThrow(/still has 20 configured stack\(s\)/);
expect(zonesRepository.softDelete).not.toHaveBeenCalled();
});
it('404s on an unknown zone', async () => {
const { service } = makeZonesService(null, 0);
await expect(service.remove('nope')).rejects.toBeInstanceOf(NotFoundException);
});
});