fix(firstleg): atomically finalize PDA completion

This commit is contained in:
2026-09-01 11:11:43 +08:00
parent de0eddeee8
commit b5bc77b153
10 changed files with 181 additions and 46 deletions
@@ -14,8 +14,10 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SpringJUnitConfig(LoadingDeliveryService.class)
class LoadingDeliveryServiceTest {
@@ -45,4 +47,76 @@ class LoadingDeliveryServiceTest {
verify(cabinetEvidence).requireComplete(99L);
}
@Test
void pdaFinalizeUsesSourceVersionIncrementedByFullSettlement() throws Exception {
Snapshot refreshed = new Snapshot(20L, 10L, "LOADING", 8);
when(repository.completionTarget(10L, 20L)).thenReturn(null);
when(repository.findInboundForUpdate(10L, 20L)).thenReturn(refreshed);
when(repository.claimOperation(eq(10L), eq(20L), eq(9L), eq("COMPLETE_LOADING"),
eq("pda-full"), anyString())).thenReturn(true);
when(repository.findInbound(10L, 20L)).thenReturn(refreshed);
when(loadingPicks.inboundProgress(10L, 20L)).thenReturn(resolvedProgress());
when(cabinetEvidence.requireContainerBookingId(10L, 20L)).thenReturn(99L);
when(repository.completeLoading(10L, 20L, 9L, 8)).thenReturn(1);
when(objectMapper.writeValueAsString(org.mockito.ArgumentMatchers.any())).thenReturn("result");
service.completeAfterPdaSettlement(10L, 20L, 9L, "operator", "pda-full", "done");
verify(repository).completeLoading(10L, 20L, 9L, 8);
}
@Test
void pdaFinalizeLocksAndUsesLoadedSliceVersionAfterPartialSettlement() throws Exception {
var target = new com.baiyue.firstleg.loading.mapper.LoadingInboundMapper.CompletionTarget(
20L, 21L, 22L, "LOADING", 6, "LOADING", 9);
Snapshot source = new Snapshot(20L, 10L, "LOADING", 6);
Snapshot loadedSlice = new Snapshot(21L, 10L, "LOADING", 9);
when(repository.completionTarget(10L, 20L)).thenReturn(target);
when(repository.findInboundForUpdate(10L, 21L)).thenReturn(loadedSlice);
when(repository.claimOperation(eq(10L), eq(20L), eq(9L), eq("COMPLETE_LOADING"),
eq("pda-partial"), anyString())).thenReturn(true);
when(repository.findInbound(10L, 20L)).thenReturn(source);
when(loadingPicks.inboundProgress(10L, 20L)).thenReturn(resolvedProgress());
when(cabinetEvidence.requireContainerBookingId(10L, 20L)).thenReturn(99L);
when(repository.completeLoadedChild(10L, 20L, 9L, 9)).thenReturn(1);
when(objectMapper.writeValueAsString(org.mockito.ArgumentMatchers.any())).thenReturn("result");
service.completeAfterPdaSettlement(10L, 20L, 9L, "operator", "pda-partial", "done");
verify(repository).findInboundForUpdate(10L, 21L);
verify(repository).completeLoadedChild(10L, 20L, 9L, 9);
}
@Test
void pdaFinalizeKeepsOptimisticConflictForRealConcurrentChange() {
Snapshot refreshed = new Snapshot(20L, 10L, "LOADING", 8);
when(repository.completionTarget(10L, 20L)).thenReturn(null);
when(repository.findInboundForUpdate(10L, 20L)).thenReturn(refreshed);
when(repository.claimOperation(eq(10L), eq(20L), eq(9L), eq("COMPLETE_LOADING"),
eq("pda-conflict"), anyString())).thenReturn(true);
when(repository.findInbound(10L, 20L)).thenReturn(refreshed);
when(loadingPicks.inboundProgress(10L, 20L)).thenReturn(resolvedProgress());
when(cabinetEvidence.requireContainerBookingId(10L, 20L)).thenReturn(99L);
when(repository.completeLoading(10L, 20L, 9L, 8)).thenReturn(0);
try {
when(objectMapper.writeValueAsString(org.mockito.ArgumentMatchers.any())).thenReturn("request");
} catch (com.fasterxml.jackson.core.JsonProcessingException exception) {
throw new AssertionError(exception);
}
assertThatThrownBy(() -> service.completeAfterPdaSettlement(10L, 20L, 9L, "operator",
"pda-conflict", "done")).isInstanceOf(com.baiyue.common.core.exception.BizException.class);
verify(repository).completeLoading(10L, 20L, 9L, 8);
verify(histories, never()).insert(org.mockito.ArgumentMatchers
.any(com.baiyue.firstleg.entity.InboundStatusHistoryEntity.class));
}
private static InboundProgress resolvedProgress() {
InboundProgress progress = new InboundProgress();
progress.setExpectedCount(2); progress.setPickedCount(2); progress.setTaskCount(1);
progress.setCompletedTaskCount(0); progress.setUnresolvedCount(0);
return progress;
}
}
@@ -44,10 +44,13 @@ import org.mockito.InOrder;
import org.mockito.ArgumentCaptor;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.time.LocalDateTime;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
@@ -74,6 +77,8 @@ class LoadingPickServiceTest {
@MockBean
private LoadingCompletionCommandRepository completionCommands;
@MockBean
private TransactionTemplate completionTransaction;
@MockBean
private SmartLoadingRepository smartLoadingRepository;
@MockBean
private OceanBookingRepository oceanBookingRepository;
@@ -100,6 +105,12 @@ class LoadingPickServiceTest {
@BeforeEach
void activeOperationalContainer() {
doAnswer(invocation -> {
@SuppressWarnings("unchecked")
Consumer<TransactionStatus> action = invocation.getArgument(0);
action.accept(mock(TransactionStatus.class));
return null;
}).when(completionTransaction).executeWithoutResult(any());
ContainerBookingEntity booking = new ContainerBookingEntity();
booking.setId(99L);
booking.setContainerNo("CSLU1234567");