mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 19:43:39 +00:00
booking operations and trains scheduling also allocations
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { autoFillPlacements, unitKey, validateLocalPlacements } from './containerPlacement.util';
|
||||
import type { ContainerUnitRow } from '@/types/trainScheduling';
|
||||
|
||||
function makeUnits(containerType: string, sizeFt: number, quantity: number): ContainerUnitRow[] {
|
||||
const units: ContainerUnitRow[] = [];
|
||||
const containersPerWagon = sizeFt >= 40 ? 1 : 2;
|
||||
const wagonsPerUnit = sizeFt >= 40 ? 1 : 0.5;
|
||||
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
units.push({
|
||||
bookingId: 'booking-1',
|
||||
bookingReference: 'BKG-001',
|
||||
bookingContainerId: 'bc-1',
|
||||
unitIndex: i,
|
||||
containerTypeId: 'ct-1',
|
||||
containerTypeCode: containerType,
|
||||
label: `${containerType} ${i + 1}/${quantity}`,
|
||||
grossWeightTons: 25,
|
||||
sizeFt,
|
||||
wagonsPerUnit,
|
||||
containersPerWagon,
|
||||
teuSlots: sizeFt >= 40 ? 2 : 1,
|
||||
});
|
||||
}
|
||||
return units;
|
||||
}
|
||||
|
||||
describe('containerPlacement.util', () => {
|
||||
describe('unitKey', () => {
|
||||
it('creates unique keys for units', () => {
|
||||
expect(unitKey('bc-1', 0)).toBe('bc-1:0');
|
||||
expect(unitKey('bc-1', 1)).toBe('bc-1:1');
|
||||
expect(unitKey('bc-2', 0)).toBe('bc-2:0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('autoFillPlacements', () => {
|
||||
it('returns empty array when no units or slots', () => {
|
||||
expect(autoFillPlacements([], [1, 2, 3])).toEqual([]);
|
||||
expect(autoFillPlacements(makeUnits('20GP', 20, 1), [])).toEqual([]);
|
||||
});
|
||||
|
||||
it('places 2×20ft containers in 1 wagon slot', () => {
|
||||
const units = makeUnits('20GP', 20, 2);
|
||||
const slots = [1, 2, 3];
|
||||
const placements = autoFillPlacements(units, slots);
|
||||
|
||||
expect(placements).toHaveLength(2);
|
||||
// Both 20ft containers should be in slot 1
|
||||
expect(placements[0]?.sequenceNo).toBe(1);
|
||||
expect(placements[1]?.sequenceNo).toBe(1);
|
||||
});
|
||||
|
||||
it('places 6×20ft containers in 3 wagon slots (2 per wagon)', () => {
|
||||
const units = makeUnits('20GP', 20, 6);
|
||||
const slots = [1, 2, 3, 4, 5];
|
||||
const placements = autoFillPlacements(units, slots);
|
||||
|
||||
expect(placements).toHaveLength(6);
|
||||
// Units 0,1 -> Slot 1
|
||||
expect(placements[0]?.sequenceNo).toBe(1);
|
||||
expect(placements[1]?.sequenceNo).toBe(1);
|
||||
// Units 2,3 -> Slot 2
|
||||
expect(placements[2]?.sequenceNo).toBe(2);
|
||||
expect(placements[3]?.sequenceNo).toBe(2);
|
||||
// Units 4,5 -> Slot 3
|
||||
expect(placements[4]?.sequenceNo).toBe(3);
|
||||
expect(placements[5]?.sequenceNo).toBe(3);
|
||||
});
|
||||
|
||||
it('places 1×40ft container in 1 wagon slot', () => {
|
||||
const units = makeUnits('40GP', 40, 1);
|
||||
const slots = [1, 2, 3];
|
||||
const placements = autoFillPlacements(units, slots);
|
||||
|
||||
expect(placements).toHaveLength(1);
|
||||
expect(placements[0]?.sequenceNo).toBe(1);
|
||||
});
|
||||
|
||||
it('places 3×40ft containers in 3 wagon slots (1 per wagon)', () => {
|
||||
const units = makeUnits('40GP', 40, 3);
|
||||
const slots = [1, 2, 3, 4, 5];
|
||||
const placements = autoFillPlacements(units, slots);
|
||||
|
||||
expect(placements).toHaveLength(3);
|
||||
// Each 40ft container gets its own slot
|
||||
expect(placements[0]?.sequenceNo).toBe(1);
|
||||
expect(placements[1]?.sequenceNo).toBe(2);
|
||||
expect(placements[2]?.sequenceNo).toBe(3);
|
||||
});
|
||||
|
||||
it('handles mixed 20ft and 40ft containers correctly', () => {
|
||||
const units20 = makeUnits('20GP', 20, 2);
|
||||
const units40 = makeUnits('40GP', 40, 1);
|
||||
const units = [...units20, ...units40];
|
||||
const slots = [1, 2, 3, 4, 5];
|
||||
const placements = autoFillPlacements(units, slots);
|
||||
|
||||
expect(placements).toHaveLength(3);
|
||||
// First two 20ft containers share slot 1
|
||||
expect(placements[0]?.sequenceNo).toBe(1);
|
||||
expect(placements[1]?.sequenceNo).toBe(1);
|
||||
// 40ft container gets slot 2
|
||||
expect(placements[2]?.sequenceNo).toBe(2);
|
||||
});
|
||||
|
||||
it('falls back to last slot when running out of slots', () => {
|
||||
const units = makeUnits('20GP', 20, 6);
|
||||
const slots = [1, 2]; // Only 2 slots available
|
||||
const placements = autoFillPlacements(units, slots);
|
||||
|
||||
expect(placements).toHaveLength(6);
|
||||
// First 4 units fit in slots 1 and 2
|
||||
expect(placements[0]?.sequenceNo).toBe(1);
|
||||
expect(placements[1]?.sequenceNo).toBe(1);
|
||||
expect(placements[2]?.sequenceNo).toBe(2);
|
||||
expect(placements[3]?.sequenceNo).toBe(2);
|
||||
// Remaining units fall back to last available slot (slot 2)
|
||||
expect(placements[4]?.sequenceNo).toBe(2);
|
||||
expect(placements[5]?.sequenceNo).toBe(2);
|
||||
});
|
||||
|
||||
it('defaults to 2 containers per wagon when sizeFt is not provided', () => {
|
||||
const units: ContainerUnitRow[] = [
|
||||
{
|
||||
bookingId: 'booking-1',
|
||||
bookingReference: 'BKG-001',
|
||||
bookingContainerId: 'bc-1',
|
||||
unitIndex: 0,
|
||||
containerTypeId: 'ct-1',
|
||||
containerTypeCode: '20GP',
|
||||
label: 'Container 1',
|
||||
grossWeightTons: 25,
|
||||
// sizeFt not provided, should default to 2 per wagon
|
||||
},
|
||||
{
|
||||
bookingId: 'booking-1',
|
||||
bookingReference: 'BKG-001',
|
||||
bookingContainerId: 'bc-1',
|
||||
unitIndex: 1,
|
||||
containerTypeId: 'ct-1',
|
||||
containerTypeCode: '20GP',
|
||||
label: 'Container 2',
|
||||
grossWeightTons: 25,
|
||||
},
|
||||
];
|
||||
const slots = [1, 2, 3];
|
||||
const placements = autoFillPlacements(units, slots);
|
||||
|
||||
expect(placements[0]?.sequenceNo).toBe(1);
|
||||
expect(placements[1]?.sequenceNo).toBe(1);
|
||||
});
|
||||
|
||||
it('uses 1 container per wagon for 40ft when sizeFt is 40', () => {
|
||||
const units: ContainerUnitRow[] = [
|
||||
{
|
||||
bookingId: 'booking-1',
|
||||
bookingReference: 'BKG-001',
|
||||
bookingContainerId: 'bc-1',
|
||||
unitIndex: 0,
|
||||
containerTypeId: 'ct-1',
|
||||
containerTypeCode: '40GP',
|
||||
label: 'Container 1',
|
||||
grossWeightTons: 25,
|
||||
sizeFt: 40,
|
||||
},
|
||||
{
|
||||
bookingId: 'booking-1',
|
||||
bookingReference: 'BKG-001',
|
||||
bookingContainerId: 'bc-1',
|
||||
unitIndex: 1,
|
||||
containerTypeId: 'ct-1',
|
||||
containerTypeCode: '40GP',
|
||||
label: 'Container 2',
|
||||
grossWeightTons: 25,
|
||||
sizeFt: 40,
|
||||
},
|
||||
];
|
||||
const slots = [1, 2, 3];
|
||||
const placements = autoFillPlacements(units, slots);
|
||||
|
||||
// Each 40ft container should get its own slot
|
||||
expect(placements[0]?.sequenceNo).toBe(1);
|
||||
expect(placements[1]?.sequenceNo).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateLocalPlacements', () => {
|
||||
it('returns empty array for valid placements', () => {
|
||||
const units = makeUnits('20GP', 20, 1);
|
||||
const placements = [
|
||||
{
|
||||
bookingContainerId: 'bc-1',
|
||||
unitIndex: 0,
|
||||
sequenceNo: 1,
|
||||
containerNumber: 'CNTR123',
|
||||
},
|
||||
];
|
||||
expect(validateLocalPlacements(units, placements)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns error for missing slot', () => {
|
||||
const units = makeUnits('20GP', 20, 1);
|
||||
const placements: ReturnType<typeof autoFillPlacements> = [];
|
||||
const issues = validateLocalPlacements(units, placements);
|
||||
expect(issues.some((i) => i.includes('Slot missing'))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns error for missing container number', () => {
|
||||
const units = makeUnits('20GP', 20, 1);
|
||||
const placements = [
|
||||
{
|
||||
bookingContainerId: 'bc-1',
|
||||
unitIndex: 0,
|
||||
sequenceNo: 1,
|
||||
// No containerNumber or containerId
|
||||
},
|
||||
];
|
||||
const issues = validateLocalPlacements(units, placements);
|
||||
expect(issues.some((i) => i.includes('Enter a container number'))).toBe(true);
|
||||
});
|
||||
|
||||
it('returns error for duplicate container numbers', () => {
|
||||
const units = makeUnits('20GP', 20, 2);
|
||||
const placements = [
|
||||
{
|
||||
bookingContainerId: 'bc-1',
|
||||
unitIndex: 0,
|
||||
sequenceNo: 1,
|
||||
containerNumber: 'CNTR123',
|
||||
},
|
||||
{
|
||||
bookingContainerId: 'bc-1',
|
||||
unitIndex: 1,
|
||||
sequenceNo: 1,
|
||||
containerNumber: 'CNTR123', // Duplicate!
|
||||
},
|
||||
];
|
||||
const issues = validateLocalPlacements(units, placements);
|
||||
expect(issues.some((i) => i.includes('Duplicate container number'))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user