fix: repair shadow candidate planning and site runtime errors
This commit is contained in:
@@ -472,6 +472,7 @@ public final class InboundPlanner {
|
||||
}
|
||||
|
||||
private boolean usableSlot(Lane lane, SpaceState space, InboundRequest request, InboundContext context) {
|
||||
if (request.eligibleSpaceIds() != null && !request.eligibleSpaceIds().contains(space.spaceId())) return false;
|
||||
if (!lane.usableForInbound() || !space.availableEmpty() || space.role() != SpaceRole.STORAGE) return false;
|
||||
if (request.targetFloor() != null && space.floorNo() != request.targetFloor()) return false;
|
||||
if (request.targetFloor() == null && !request.allowedFloors().isEmpty()
|
||||
|
||||
@@ -12,8 +12,16 @@ public record InboundRequest(
|
||||
Integer entryFloor,
|
||||
Integer targetFloor,
|
||||
Set<Integer> allowedFloors,
|
||||
BatchLaneMode batchLaneMode
|
||||
BatchLaneMode batchLaneMode,
|
||||
Set<String> eligibleSpaceIds
|
||||
) {
|
||||
public InboundRequest(String requestId, String trayNo, String projectNo, String productId, String lotNo,
|
||||
Integer expectedProjectPallets, Integer entryFloor, Integer targetFloor,
|
||||
Set<Integer> allowedFloors, BatchLaneMode batchLaneMode) {
|
||||
this(requestId, trayNo, projectNo, productId, lotNo, expectedProjectPallets, entryFloor, targetFloor,
|
||||
allowedFloors, batchLaneMode, null);
|
||||
}
|
||||
|
||||
public InboundRequest {
|
||||
if (requestId == null || requestId.isBlank()) throw new IllegalArgumentException("requestId is required");
|
||||
if (trayNo == null || trayNo.isBlank()) throw new IllegalArgumentException("trayNo is required");
|
||||
@@ -22,5 +30,7 @@ public record InboundRequest(
|
||||
}
|
||||
allowedFloors = allowedFloors == null ? Set.of() : Set.copyOf(allowedFloors);
|
||||
batchLaneMode = batchLaneMode == null ? BatchLaneMode.OPEN : batchLaneMode;
|
||||
// null means unrestricted; an empty captured set means no eligible destination.
|
||||
eligibleSpaceIds = eligibleSpaceIds == null ? null : Set.copyOf(eligibleSpaceIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,14 @@ public final class SchedulingFacade {
|
||||
return applicationService.planOutboundFromPublishedSnapshot(warehouseId, request);
|
||||
}
|
||||
|
||||
public OutboundPlan planShadowOutboundCandidates(String warehouseId, OutboundRequest request,
|
||||
java.util.Set<String> eligibleStockIds) {
|
||||
if (properties.getMode() != SchedulingMode.SHADOW) {
|
||||
throw new IllegalStateException("Captured candidate planning requires SHADOW mode");
|
||||
}
|
||||
return applicationService.planShadowOutboundCandidates(warehouseId, request, eligibleStockIds);
|
||||
}
|
||||
|
||||
public TidyPlan planTidy(String warehouseId, Instant now) {
|
||||
requireAlgorithmMode();
|
||||
return applicationService.planTidy(warehouseId, now);
|
||||
|
||||
@@ -18,7 +18,14 @@ import java.util.Optional;
|
||||
public final class OutboundPlanner {
|
||||
|
||||
public OutboundPlan plan(OutboundRequest request, OutboundContext context) {
|
||||
return plan(request, context, null);
|
||||
}
|
||||
|
||||
public OutboundPlan plan(OutboundRequest request, OutboundContext context, java.util.Set<String> eligibleSpaceIds) {
|
||||
String mode = normalizedMode(request.fifoMode());
|
||||
if (eligibleSpaceIds != null && !"OFF".equals(mode)) {
|
||||
throw new IllegalArgumentException("Captured candidate planning supports FIFO OFF only");
|
||||
}
|
||||
if (!List.of("OFF", "STRICT", "FLEXIBLE").contains(mode)) {
|
||||
return error(OutboundErrorCode.FIFO_INVALID_MODE, "Unsupported FIFO mode",
|
||||
Map.of("fifoMode", String.valueOf(request.fifoMode())));
|
||||
@@ -35,6 +42,7 @@ public final class OutboundPlanner {
|
||||
|
||||
List<SpaceState> physicalMatches = context.spaces().stream()
|
||||
.filter(SpaceState::occupied)
|
||||
.filter(space -> eligibleSpaceIds == null || eligibleSpaceIds.contains(space.spaceId()))
|
||||
.filter(space -> matchesRequest(space, request))
|
||||
.toList();
|
||||
if (physicalMatches.isEmpty()) {
|
||||
|
||||
@@ -359,6 +359,17 @@ public final class JdbcWarehouseRepository implements WarehouseRepository {
|
||||
|
||||
@Override
|
||||
public List<LaneSnapshot> loadLaneSnapshots(String warehouseId, Collection<String> laneCodes) {
|
||||
return loadLaneSnapshots(warehouseId, laneCodes, Set.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LaneSnapshot> loadShadowOutboundLaneSnapshots(String warehouseId, Collection<String> laneCodes,
|
||||
Set<String> candidateSpaceIds) {
|
||||
return loadLaneSnapshots(warehouseId, laneCodes, Set.copyOf(candidateSpaceIds));
|
||||
}
|
||||
|
||||
private List<LaneSnapshot> loadLaneSnapshots(String warehouseId, Collection<String> laneCodes,
|
||||
Set<String> mixedProjectSources) {
|
||||
LinkedHashSet<String> codes = laneCodes == null ? new LinkedHashSet<>()
|
||||
: laneCodes.stream().filter(Objects::nonNull).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (codes.isEmpty()) return List.of();
|
||||
@@ -393,7 +404,7 @@ public final class JdbcWarehouseRepository implements WarehouseRepository {
|
||||
int capacity = entry.getValue().size();
|
||||
List<SpaceSnapshot> spaces = entry.getValue().stream()
|
||||
.map(raw -> mapSpace(raw, capacity, versions.get(entry.getKey()),
|
||||
inventoryBySpace.getOrDefault(raw.spaceId(), List.of())))
|
||||
inventoryBySpace.getOrDefault(raw.spaceId(), List.of()), mixedProjectSources.contains(raw.spaceId())))
|
||||
.sorted(Comparator.comparingInt((SpaceSnapshot value) -> value.state().depthOrder())
|
||||
.thenComparing(SpaceSnapshot::spaceId)).toList();
|
||||
result.add(new LaneSnapshot(warehouseId, entry.getKey(), versions.get(entry.getKey()), spaces));
|
||||
@@ -720,6 +731,11 @@ public final class JdbcWarehouseRepository implements WarehouseRepository {
|
||||
}
|
||||
|
||||
private SpaceSnapshot mapSpace(RawSpace raw, int capacity, VersionToken version, List<InventoryItem> inventory) {
|
||||
return mapSpace(raw, capacity, version, inventory, false);
|
||||
}
|
||||
|
||||
private SpaceSnapshot mapSpace(RawSpace raw, int capacity, VersionToken version,
|
||||
List<InventoryItem> inventory, boolean allowMixedProjectSource) {
|
||||
EnumSet<SpaceConflict> conflicts = EnumSet.noneOf(SpaceConflict.class);
|
||||
if (raw.trayConflict()) conflicts.add(SpaceConflict.TRAY);
|
||||
if (raw.projectConflict()) conflicts.add(SpaceConflict.PROJECT);
|
||||
@@ -734,7 +750,8 @@ public final class JdbcWarehouseRepository implements WarehouseRepository {
|
||||
Instant inboundAt = inventory.stream().map(InventoryItem::inboundAt).filter(Objects::nonNull)
|
||||
.min(Comparator.naturalOrder()).orElse(null);
|
||||
boolean invalid = locked(raw.locked()) || raw.reserved() || raw.hasGap() || raw.depth() == null
|
||||
|| !conflicts.isEmpty() || ("1".equals(raw.spaceStatus()) && (!inventory.isEmpty() || tray != null));
|
||||
|| conflicts.stream().anyMatch(conflict -> conflict != SpaceConflict.PROJECT || !allowMixedProjectSource)
|
||||
|| ("1".equals(raw.spaceStatus()) && (!inventory.isEmpty() || tray != null));
|
||||
SpaceStatus status = invalid ? SpaceStatus.DISABLED : switch (Objects.toString(raw.spaceStatus(), "")) {
|
||||
case "1" -> SpaceStatus.EMPTY;
|
||||
case "2" -> SpaceStatus.OCCUPIED;
|
||||
|
||||
@@ -16,6 +16,10 @@ public interface WarehouseRepository {
|
||||
LanePage pageCandidateLanes(String warehouseId, String afterLaneCode, int pageSize);
|
||||
LanePage pageWarehouseLanes(String warehouseId, String afterLaneCode, int pageSize);
|
||||
List<LaneSnapshot> loadLaneSnapshots(String warehouseId, Collection<String> laneCodes);
|
||||
default List<LaneSnapshot> loadShadowOutboundLaneSnapshots(String warehouseId, Collection<String> laneCodes,
|
||||
Set<String> candidateSpaceIds) {
|
||||
return loadLaneSnapshots(warehouseId, laneCodes);
|
||||
}
|
||||
List<SpaceSnapshot> loadRelocationBuffers(String warehouseId, Set<Integer> floors);
|
||||
Set<Integer> loadActiveDeepWorkflowFloors(String warehouseId, String excludingRequestId);
|
||||
boolean acquireDeepWorkflowLock(String requestId, String warehouseId, int floorNo);
|
||||
|
||||
@@ -98,7 +98,8 @@ public final class WarehouseApplicationService {
|
||||
int currentProjectPallets = repository.countProjectPallets(warehouseId, request.projectNo());
|
||||
effectiveRequest = new InboundRequest(request.requestId(), request.trayNo(), request.projectNo(),
|
||||
request.productId(), request.lotNo(), currentProjectPallets <= 1 ? 2 : 4,
|
||||
request.entryFloor(), request.targetFloor(), request.allowedFloors(), request.batchLaneMode());
|
||||
request.entryFloor(), request.targetFloor(), request.allowedFloors(), request.batchLaneMode(),
|
||||
request.eligibleSpaceIds());
|
||||
}
|
||||
List<PendingOutbound> pending = repository.loadPendingOutboundJobs(warehouseId).stream()
|
||||
.map(job -> new PendingOutbound(job.projectNo(), job.productNo(), job.trayNo(),
|
||||
@@ -136,6 +137,18 @@ public final class WarehouseApplicationService {
|
||||
return planOutbound(warehouseId, request, false);
|
||||
}
|
||||
|
||||
/** The caller holds a published snapshot lease and supplies the site's eligible inventory IDs. */
|
||||
public OutboundPlan planShadowOutboundCandidates(String warehouseId, OutboundRequest request,
|
||||
Set<String> eligibleStockIds) {
|
||||
Objects.requireNonNull(eligibleStockIds, "eligibleStockIds");
|
||||
if (eligibleStockIds.size() > 200 || request.wholeTrayOrDefault() || nonBlank(request.trayNo())
|
||||
|| !nonBlank(request.projectNo()) || !nonBlank(request.productId())
|
||||
|| (nonBlank(request.fifoMode()) && !"OFF".equalsIgnoreCase(request.fifoMode()))) {
|
||||
throw new IllegalArgumentException("Shadow candidates require a bounded project/product picking scope with FIFO OFF");
|
||||
}
|
||||
return planOutboundFromCurrentReadModel(warehouseId, request, Set.copyOf(eligibleStockIds));
|
||||
}
|
||||
|
||||
private OutboundPlan planOutbound(String warehouseId, OutboundRequest request, boolean refresh) {
|
||||
if (refresh) {
|
||||
repository.refreshChangedLanes(warehouseId);
|
||||
@@ -144,6 +157,11 @@ public final class WarehouseApplicationService {
|
||||
}
|
||||
|
||||
private OutboundPlan planOutboundFromCurrentReadModel(String warehouseId, OutboundRequest request) {
|
||||
return planOutboundFromCurrentReadModel(warehouseId, request, null);
|
||||
}
|
||||
|
||||
private OutboundPlan planOutboundFromCurrentReadModel(String warehouseId, OutboundRequest request,
|
||||
Set<String> eligibleStockIds) {
|
||||
RepositoryPolicy policy = repository.loadPolicy(warehouseId);
|
||||
WarehouseSummary summary = repository.loadSummary(warehouseId);
|
||||
OutboundRequest effectiveRequest = request.fifoToleranceMinutes() == null && policy.fifoToleranceMinutes() > 0
|
||||
@@ -187,18 +205,28 @@ public final class WarehouseApplicationService {
|
||||
matches.addAll(repository.searchOutboundStock(warehouseId,
|
||||
OutboundStockSearch.project(request.projectNo(), request.productId())));
|
||||
}
|
||||
if (eligibleStockIds != null) {
|
||||
matches.removeIf(match -> !eligibleStockIds.contains(match.stockId()));
|
||||
Set<String> resolved = matches.stream().map(OutboundStockMatch::stockId).collect(java.util.stream.Collectors.toSet());
|
||||
if (!resolved.equals(eligibleStockIds)) {
|
||||
throw new IllegalStateException("Captured outbound candidate is missing from the published inventory");
|
||||
}
|
||||
}
|
||||
Set<String> matchedSpaceIds = matches.stream().map(OutboundStockMatch::spaceId)
|
||||
.filter(this::nonBlank).collect(java.util.stream.Collectors.toSet());
|
||||
List<String> laneCodes = matches.stream().map(OutboundStockMatch::laneCode).filter(Objects::nonNull)
|
||||
.distinct().sorted().toList();
|
||||
List<LaneSnapshot> lanes = repository.loadLaneSnapshots(warehouseId, laneCodes);
|
||||
List<LaneSnapshot> lanes = eligibleStockIds == null ? repository.loadLaneSnapshots(warehouseId, laneCodes)
|
||||
: repository.loadShadowOutboundLaneSnapshots(warehouseId, laneCodes, matchedSpaceIds);
|
||||
Set<Integer> floors = matches.stream().map(OutboundStockMatch::floorNo)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
List<SpaceSnapshot> buffers = repository.loadRelocationBuffers(warehouseId, floors);
|
||||
Map<String, SpaceState> uniqueSpaces = new LinkedHashMap<>();
|
||||
Set<String> matchedSpaceIds = matches.stream().map(OutboundStockMatch::spaceId)
|
||||
.filter(this::nonBlank).collect(java.util.stream.Collectors.toSet());
|
||||
String productScope = matchedProductScope;
|
||||
lanes.stream().flatMap(lane -> lane.spaceStates().stream()).forEach(space -> uniqueSpaces.put(space.spaceId(),
|
||||
productScope != null && matchedSpaceIds.contains(space.spaceId())
|
||||
eligibleStockIds != null && matchedSpaceIds.contains(space.spaceId())
|
||||
? withInventoryScope(space, request.projectNo(), request.productId())
|
||||
: productScope != null && matchedSpaceIds.contains(space.spaceId())
|
||||
? withProductScope(space, productScope) : space));
|
||||
buffers.stream().map(SpaceSnapshot::state).forEach(space -> uniqueSpaces.put(space.spaceId(), space));
|
||||
Map<String, String> rawInboundTimes = new LinkedHashMap<>();
|
||||
@@ -210,7 +238,8 @@ public final class WarehouseApplicationService {
|
||||
OutboundPlan plan = outboundPlanner.plan(effectiveRequest,
|
||||
new OutboundContext(List.copyOf(uniqueSpaces.values()), rawInboundTimes,
|
||||
capacityMode(summary, policy),
|
||||
repository.loadActiveDeepWorkflowFloors(warehouseId, request.requestId())));
|
||||
repository.loadActiveDeepWorkflowFloors(warehouseId, request.requestId())),
|
||||
eligibleStockIds == null ? null : matchedSpaceIds);
|
||||
String resultCode = switch (plan.status()) {
|
||||
case READY -> "READY";
|
||||
case WAITING -> "WAITING_" + plan.waiting().orElseThrow().code().name();
|
||||
@@ -312,10 +341,14 @@ public final class WarehouseApplicationService {
|
||||
}
|
||||
|
||||
private SpaceState withProductScope(SpaceState space, String productId) {
|
||||
return withInventoryScope(space, space.projectNo(), productId);
|
||||
}
|
||||
|
||||
private SpaceState withInventoryScope(SpaceState space, String projectNo, String productId) {
|
||||
return new SpaceState(space.spaceId(), space.zoneCode(), space.laneCode(), space.depthOrder(),
|
||||
space.laneCapacity(), space.floorNo(), space.aisleNo(), space.groupCoding(), space.rowCode(),
|
||||
space.role(), space.status(), space.outboundTaskActive(), space.inventoryProjectConflict(),
|
||||
space.trayNo(), space.projectNo(), productId, space.lotNo(), space.inboundAt(), space.outboundAt(),
|
||||
space.trayNo(), projectNo, productId, space.lotNo(), space.inboundAt(), space.outboundAt(),
|
||||
space.reserved(), space.rowVersion(), space.physicalStockPresent());
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +104,33 @@ class JdbcWarehouseRepositoryTest {
|
||||
assertThat(repository.countProjectPallets("W1", "PROJECT-W2")).isZero();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mixedProjectPalletIsSelectableByItsEligibleInventoryItem() throws Exception {
|
||||
execute("INSERT INTO BAS_CARGO_SPACE VALUES ('MIXED-SOURCE','W1','R','A','Mixed pallet',9,91,1,'2','',"
|
||||
+ "'MIXED-TRAY','MIXED-LANE','1')");
|
||||
execute("INSERT INTO BAS_STOCK VALUES ('MIX-1','MIXED-SOURCE','MIXED-TRAY','P1','SKU1','LOT','2026-01-01 09:00:00',5,0)");
|
||||
execute("INSERT INTO BAS_STOCK VALUES ('MIX-2','MIXED-SOURCE','MIXED-TRAY','P2','SKU2','LOT','2026-01-01 09:00:00',5,0)");
|
||||
rebuildTopology("W1");
|
||||
var service = new WarehouseApplicationService(repository, new InboundPlanner(), new OutboundPlanner(), new TidyPlanner(), 100);
|
||||
var request = new OutboundRequest("MIX-PICK", null, "P1", "SKU1", "OFF", null, 1, false);
|
||||
|
||||
var plan = service.planShadowOutboundCandidates("W1", request, Set.of("MIX-1"));
|
||||
|
||||
assertThat(plan.status()).isEqualTo(OutboundPlanStatus.READY);
|
||||
assertThat(plan.assignment().orElseThrow().sourceSpaceId()).isEqualTo("MIXED-SOURCE");
|
||||
assertThat(plan.assignment().orElseThrow().projectNo()).isEqualTo("P1");
|
||||
assertThat(plan.assignment().orElseThrow().productId()).isEqualTo("SKU1");
|
||||
assertThat(space(repository.loadLaneSnapshots("W1", List.of("MIXED-LANE")), "MIXED-SOURCE")
|
||||
.state().outboundSelectable()).isFalse();
|
||||
for (String columnValue : List.of("locked='Y'", "status='4'", "tray_code='WRONG'")) {
|
||||
execute("UPDATE BAS_CARGO_SPACE SET " + columnValue + " WHERE cargo_space_id='MIXED-SOURCE'");
|
||||
rebuildTopology("W1");
|
||||
assertThat(service.planShadowOutboundCandidates("W1", request, Set.of("MIX-1")).status())
|
||||
.isNotEqualTo(OutboundPlanStatus.READY);
|
||||
execute("UPDATE BAS_CARGO_SPACE SET locked='',status='2',tray_code='MIXED-TRAY' WHERE cargo_space_id='MIXED-SOURCE'");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusFourPhysicalOuterPalletIsPreservedAndMakesDeepOutboundWait() throws Exception {
|
||||
execute("INSERT INTO BAS_CARGO_SPACE VALUES ('ALLOC-TARGET','W1','RA','A','Target',7,21,2,'2','',"
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.ruoyi.algorithm.service;
|
||||
|
||||
import com.ruoyi.algorithm.inbound.*;
|
||||
import com.ruoyi.algorithm.integration.*;
|
||||
import com.ruoyi.algorithm.model.*;
|
||||
import com.ruoyi.algorithm.outbound.*;
|
||||
import com.ruoyi.algorithm.repository.*;
|
||||
import com.ruoyi.algorithm.tidy.TidyPlanner;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
|
||||
class ShadowCandidatePlanningTest {
|
||||
@Test
|
||||
void ineligibleOuterStockRemainsAPhysicalBlockerInsteadOfBeingChosenOrRemoved() {
|
||||
var repository = mock(WarehouseRepository.class);
|
||||
when(repository.loadPolicy("01")).thenReturn(RepositoryPolicy.defaults());
|
||||
when(repository.loadSummary("01")).thenReturn(new WarehouseSummary("01", 1, 2, 2, 0, 0, 0, 0));
|
||||
when(repository.searchOutboundStock(eq("01"), any())).thenReturn(List.of(
|
||||
new OutboundStockMatch("INNER", "L-1", "L", 1, "T1", "P", "SKU", "LOT", null, null),
|
||||
new OutboundStockMatch("OUTER", "L-2", "L", 1, "T2", "P", "SKU", "LOT", null, null)));
|
||||
var source = state("L", 1, SpaceStatus.OCCUPIED, "T1", null);
|
||||
var blocker = state("L", 2, SpaceStatus.OCCUPIED, "T2", "P");
|
||||
when(repository.loadShadowOutboundLaneSnapshots("01", List.of("L"), Set.of("L-1")))
|
||||
.thenReturn(List.of(lane("L", source, blocker)));
|
||||
var service = new WarehouseApplicationService(repository, new InboundPlanner(), new OutboundPlanner(), new TidyPlanner(), 100);
|
||||
var request = new OutboundRequest("O", null, "P", "SKU", "OFF", null, 1, false);
|
||||
|
||||
var plan = service.planShadowOutboundCandidates("01", request, Set.of("INNER"));
|
||||
|
||||
assertThat(plan.waiting()).isPresent();
|
||||
assertThat(plan.waiting().orElseThrow().code()).isEqualTo(OutboundWaitingCode.WAITING_RELOCATION_BUFFER);
|
||||
assertThat(plan.waiting().orElseThrow().details().get("sourceSpaceId")).isEqualTo("L-1");
|
||||
verify(repository, never()).synchronizeReadModel(any());
|
||||
verify(repository, never()).refreshChangedLanes(any());
|
||||
verify(repository, never()).tryReserveInbound(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void eligibleInboundSetSurvivesProjectSizeResolutionAndPageBoundaries() {
|
||||
var repository = mock(WarehouseRepository.class);
|
||||
when(repository.loadPolicy("01")).thenReturn(RepositoryPolicy.defaults());
|
||||
when(repository.loadSummary("01")).thenReturn(new WarehouseSummary("01", 2, 4, 0, 4, 0, 0, 0));
|
||||
when(repository.pageCandidateLanes("01", null, 1)).thenReturn(new LanePage(
|
||||
List.of(new LaneCandidate("01", "A", 2, 0, 2, false, false, VersionToken.numeric(1))), "A"));
|
||||
when(repository.pageCandidateLanes("01", "A", 1)).thenReturn(new LanePage(
|
||||
List.of(new LaneCandidate("01", "Z", 2, 0, 2, false, false, VersionToken.numeric(2))), null));
|
||||
when(repository.loadLaneSnapshots("01", List.of("A"))).thenReturn(List.of(lane("A",
|
||||
state("A", 1, SpaceStatus.EMPTY, null, null), state("A", 2, SpaceStatus.EMPTY, null, null))));
|
||||
when(repository.loadLaneSnapshots("01", List.of("Z"))).thenReturn(List.of(lane("Z",
|
||||
state("Z", 1, SpaceStatus.EMPTY, null, null), state("Z", 2, SpaceStatus.EMPTY, null, null))));
|
||||
var service = new WarehouseApplicationService(repository, new InboundPlanner(), new OutboundPlanner(), new TidyPlanner(), 1);
|
||||
var request = new InboundRequest("I", "T", "P", null, null, null, 1, null, Set.of(), BatchLaneMode.OPEN, Set.of("Z-1"));
|
||||
|
||||
assertThat(service.planInboundFromPublishedSnapshot("01", request).plan().assignment().orElseThrow().spaceId())
|
||||
.isEqualTo("Z-1");
|
||||
var forbidden = new InboundRequest("I", "T", "P", null, null, null, 1, null, Set.of(), BatchLaneMode.OPEN, Set.of());
|
||||
assertThat(service.planInboundFromPublishedSnapshot("01", forbidden).plan().assignment()).isEmpty();
|
||||
verify(repository, never()).refreshChangedLanes(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidateProjectionCannotBeCalledThroughLegacyOrActiveFacade() {
|
||||
for (var mode : List.of(SchedulingMode.LEGACY, SchedulingMode.ACTIVE)) {
|
||||
var props = new AlgorithmProperties();
|
||||
props.setMode(mode);
|
||||
var service = mock(WarehouseApplicationService.class);
|
||||
var facade = new SchedulingFacade(service, props);
|
||||
assertThatThrownBy(() -> facade.planShadowOutboundCandidates("01",
|
||||
new OutboundRequest("O", null, "P", "SKU", "OFF", null, 1, false), Set.of("S")))
|
||||
.isInstanceOf(IllegalStateException.class).hasMessageContaining("SHADOW");
|
||||
verifyNoInteractions(service);
|
||||
}
|
||||
}
|
||||
|
||||
private LaneSnapshot lane(String code, SpaceState... spaces) {
|
||||
return new LaneSnapshot("01", code, VersionToken.numeric(1), java.util.Arrays.stream(spaces)
|
||||
.map(s -> new SpaceSnapshot(s, List.of(), Set.of(), s.availableEmpty(), false, null)).toList());
|
||||
}
|
||||
|
||||
private SpaceState state(String lane, int depth, SpaceStatus status, String tray, String project) {
|
||||
return new SpaceState(lane + "-" + depth, "A", lane, depth, 2, 1, 1, depth, "1", SpaceRole.STORAGE,
|
||||
status, false, false, tray, project, "SKU", "LOT", Instant.EPOCH, null, false, null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user