diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/bridge/service/LoadingModelBridgeServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/bridge/service/LoadingModelBridgeServiceTest.java index dc2210c10..c7690201a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/bridge/service/LoadingModelBridgeServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/bridge/service/LoadingModelBridgeServiceTest.java @@ -18,6 +18,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.LocalDateTime; import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -328,6 +332,57 @@ class LoadingModelBridgeServiceTest { anyInt(), anyLong(), anyString()); } + @ParameterizedTest + @ValueSource(ints = {1, 3}) + void reassignAfterOneOrRepeatedFullRemovalsCanComplete(int removals) { + Map links = new LinkedHashMap<>(); + Map quantities = new LinkedHashMap<>(); + Map statuses = new LinkedHashMap<>(); + when(repository.byPair(eq(10L), eq(20L), anyLong())) + .thenAnswer(call -> links.get(call.getArgument(2, Long.class))); + when(repository.allocatedPackages(eq(10L), eq(20L), anyLong())) + .thenAnswer(call -> quantities.get(call.getArgument(2, Long.class))); + when(repository.loadingPlan(anyLong())).thenAnswer(call -> { + long plan = call.getArgument(0); + return new LoadingPlanSnapshot(plan, plan + 100, "C" + plan, statuses.get(plan), 1); + }); + when(repository.preload(10L, 20L)).thenReturn(new PreloadSnapshot(30L, 10L, 20L, "CONFIRMED", 1)); + doAnswer(call -> { + PreloadLoadingPlanLinkEntity link = call.getArgument(0); + links.put(link.getLoadingPlanId(), link); + return null; + }).when(repository).insert(any()); + doAnswer(call -> { + PreloadLoadingPlanLinkEntity link = call.getArgument(0); + links.remove(link.getLoadingPlanId()); + return null; + }).when(repository).delete(any()); + when(repository.byInbound(10L, 20L)).thenAnswer(call -> links.keySet().stream() + .map(plan -> new Link(plan, 10L, 20L, 30L, "CONFIRMED", 1, plan, plan + 100, + "C" + plan, statuses.get(plan), 1, quantities.get(plan), LocalDateTime.now())).toList()); + + for (long plan = 40; plan < 40 + removals; plan++) { + quantities.put(plan, 8); + statuses.put(plan, "EDITING"); + service.bindIncrementalAllocation(plan, 10L, 20L, 7L, "admin", "assign-" + plan); + assertThat(links).containsKey(plan); + quantities.put(plan, 3); + service.removeIfUnallocated(plan, 10L, 20L); + assertThat(links).containsKey(plan); + quantities.put(plan, 0); + service.removeIfUnallocated(plan, 10L, 20L); + assertThat(links).isEmpty(); + } + quantities.put(90L, 8); + statuses.put(90L, "LOADING"); + service.bindIncrementalAllocation(90L, 10L, 20L, 7L, "admin", "assign-final"); + service.requireStarted(10L, 20L); + statuses.put(90L, "COMPLETED"); + service.requireCompleted(10L, 20L); + assertThat(links).containsOnlyKeys(90L); + verify(repository, times(removals)).delete(any()); + } + private static Link link(String preloadStatus, int preloadVersion, String loadingStatus, int loadingVersion, int packages) { return new Link(1L, 10L, 20L, 30L, preloadStatus, preloadVersion, 40L, 50L, "C1", diff --git a/firstleg/src/test/java/com/baiyue/firstleg/loading/service/LoadingDeliveryServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/loading/service/LoadingDeliveryServiceTest.java index 4a6763a34..bfc207ab1 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/loading/service/LoadingDeliveryServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/loading/service/LoadingDeliveryServiceTest.java @@ -55,6 +55,7 @@ class LoadingDeliveryServiceTest { @Test void pdaFinalizeUsesSourceVersionIncrementedByFullSettlement() throws Exception { + useRealBridgeWithRemovedAndCurrentAllocations(0); Snapshot refreshed = new Snapshot(20L, 10L, "LOADING", 8); when(repository.completionTarget(10L, 20L)).thenReturn(null); when(repository.findInboundForUpdate(10L, 20L)).thenReturn(refreshed); @@ -66,11 +67,45 @@ class LoadingDeliveryServiceTest { when(repository.completeLoading(10L, 20L, 9L, 8)).thenReturn(1); when(objectMapper.writeValueAsString(org.mockito.ArgumentMatchers.any())).thenReturn("result"); + service.requirePdaCompletionReady(10L, 20L); service.completeAfterPdaSettlement(10L, 20L, 9L, "operator", "pda-full", "done"); verify(repository).completeLoading(10L, 20L, 9L, 8); } + @Test + void pdaPreflightBlocksAnUnstartedContainerWithRealAllocationBeforeAnyCompletionWrite() { + useRealBridgeWithRemovedAndCurrentAllocations(3); + when(repository.findInbound(10L, 20L)).thenReturn(new Snapshot(20L, 10L, "LOADING", 8)); + + assertThatThrownBy(() -> service.requirePdaCompletionReady(10L, 20L)) + .hasMessageContaining("关联柜次尚未全部开始装柜"); + + verify(repository, never()).completeLoading( + org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(), + org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyInt()); + verify(completionGate, never()).requireReleasedBooking(10L, 20L); + } + + /** Exercise the production bridge gate, rather than mocking its decision. */ + private void useRealBridgeWithRemovedAndCurrentAllocations(int oldContainerPackages) { + var bridgeRepository = org.mockito.Mockito.mock( + com.baiyue.firstleg.loading.bridge.repository.LoadingModelBridgeRepository.class); + var realBridge = new LoadingModelBridgeService(); + org.springframework.test.util.ReflectionTestUtils.setField(realBridge, "repository", bridgeRepository); + when(bridgeRepository.byInbound(10L, 20L)).thenReturn(java.util.List.of( + new com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link( + 1L, 10L, 20L, 30L, "CONFIRMED", 1, 40L, 50L, null, + "EDITING", 1, oldContainerPackages, java.time.LocalDateTime.now()), + new com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link( + 2L, 10L, 20L, 30L, "CONFIRMED", 1, 41L, 99L, "C1", + "LOADING", 1, 2, java.time.LocalDateTime.now()))); + org.mockito.Mockito.doAnswer(call -> { + realBridge.requireStarted(10L, 20L); + return null; + }).when(loadingModelBridge).requireStarted(10L, 20L); + } + @Test void pdaFinalizeLocksAndUsesLoadedSliceVersionAfterPartialSettlement() throws Exception { var target = new com.baiyue.firstleg.loading.mapper.LoadingInboundMapper.CompletionTarget(