优化
This commit is contained in:
+19
@@ -34,5 +34,24 @@ class PlatformSmartLoadingControllerContractTest {
|
||||
.contains("领取后才进入 LOADING")
|
||||
.doesNotContain("真实柜号必须已录入");
|
||||
}
|
||||
@Test void openApiFreezesIncrementalMutationAndCapabilityContract() throws Exception {
|
||||
String yaml=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/openapi/firstleg-v1.yaml"));
|
||||
assertThat(yaml)
|
||||
.contains("/api/platform/v1/first-leg/smart-loading/plans/{planId}/assignments:")
|
||||
.contains("/api/platform/v1/first-leg/smart-loading/plans/{planId}/items/{itemId}:")
|
||||
.contains("SmartLoadingAssignRequest:")
|
||||
.contains("SmartLoadingAdjustRequest:")
|
||||
.contains("SmartLoadingDeleteRequest:")
|
||||
.contains("canAssign: {type: boolean}")
|
||||
.contains("canEdit: {type: boolean}")
|
||||
.contains("pickedPackageCount: {type: integer, minimum: 0}")
|
||||
.contains("minPackageCount: {type: integer, minimum: 1}")
|
||||
.contains("canAdjust: {type: boolean}")
|
||||
.contains("canRemove: {type: boolean}")
|
||||
.contains("原 inventorySnapshotToken 随即失效")
|
||||
.contains("PDA task 行锁 -> plan 乐观锁")
|
||||
.contains("回到 IN_PROGRESS")
|
||||
.contains("COMPLETED 永久拒绝增量");
|
||||
}
|
||||
private void authority(String method,String permission,Class<?>...types)throws Exception{Method m=PlatformSmartLoadingController.class.getDeclaredMethod(method,types);assertThat(m.getAnnotation(PreAuthorize.class).value()).isEqualTo("hasAuthority('"+permission+"')");}
|
||||
}
|
||||
|
||||
+2
@@ -18,7 +18,9 @@ class LoadingCompletionGateSqlContractTest {
|
||||
.contains("fl_ocean_booking_container_allocation")
|
||||
.contains("b.status='RELEASED'")
|
||||
.contains("fl_truck_appointment")
|
||||
.contains("t.container_booking_id=p.container_booking_id")
|
||||
.contains("t.status='COMPLETED'")
|
||||
.doesNotContain("t.loading_plan_id=p.id")
|
||||
.contains("l.tenant_id=#{tenantId} AND l.inbound_id=#{inboundId}")
|
||||
.doesNotContain("truckReady, FROM")
|
||||
.doesNotContain("fl_customs_case", "fl_customs_receipt", "customsReady")
|
||||
|
||||
+38
@@ -102,6 +102,44 @@ class LoadingDeliveryServiceTest {
|
||||
verify(histories).insert(any(InboundStatusHistoryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pdaCompletionPreflightChecksStartedPlanTransportAndEvidenceWithoutWriting() {
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
|
||||
when(repository.listEvidence(10, 20)).thenReturn(List.of(
|
||||
evidence("PHOTO_STEP_1", 1L, null), evidence("PHOTO_STEP_2", 2L, null),
|
||||
evidence("PHOTO_STEP_3", 3L, null), evidence("PHOTO_STEP_4", 4L, null),
|
||||
evidence("SEAL", 5L, "LOCK-001")));
|
||||
when(files.validate(eq("token"), eq(10L), eq(20L), anyLong())).thenAnswer(invocation -> {
|
||||
long fileId = invocation.getArgument(3);
|
||||
String type = fileId == 5L ? "SEAL" : "PHOTO_STEP_" + fileId;
|
||||
return ApiResponse.success(new LoadingEvidenceFileClient.FileMetadata(
|
||||
fileId, type.toLowerCase() + ".png", "LOADING_" + type, "image/png"));
|
||||
});
|
||||
|
||||
service.requirePdaCompletionReady(10, 20);
|
||||
|
||||
verify(loadingModelBridge).requireStarted(10, 20);
|
||||
verify(completionGate).requireBookingAndTransport(10, 20);
|
||||
verify(loadingModelBridge, never()).requireCompleted(anyLong(), anyLong());
|
||||
verify(repository, never()).completeLoading(anyLong(), anyLong(), anyLong(), anyInt());
|
||||
verify(repository, never()).claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pdaCompletionPreflightReportsTruckBlockerBeforeEvidenceOrWrites() {
|
||||
when(repository.findInbound(10, 20)).thenReturn(new Snapshot(20L, 10L, "LOADING", 3));
|
||||
doThrow(new com.baiyue.common.core.exception.BizException(
|
||||
com.baiyue.common.core.api.ResultCode.CONFLICT,
|
||||
"LOADED门禁未通过:拖车尚未完成(必须为 COMPLETED)"))
|
||||
.when(completionGate).requireBookingAndTransport(10, 20);
|
||||
|
||||
assertThatThrownBy(() -> service.requirePdaCompletionReady(10, 20))
|
||||
.hasMessageContaining("拖车尚未完成");
|
||||
|
||||
verify(repository, never()).listEvidence(anyLong(), anyLong());
|
||||
verify(repository, never()).completeLoading(anyLong(), anyLong(), anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completesLoadedFulfillmentSliceBeforeAggregatingRoot() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
||||
import com.baiyue.firstleg.loading.service.LoadingDeliveryService;
|
||||
import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
|
||||
import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
@@ -43,7 +44,10 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -64,6 +68,8 @@ class LoadingPickServiceTest {
|
||||
@MockBean
|
||||
private LoadingListService loadingListService;
|
||||
@MockBean
|
||||
private LoadingDeliveryService loadingDeliveryService;
|
||||
@MockBean
|
||||
private InboundStatusHistoryMapper inboundHistories;
|
||||
@MockBean
|
||||
private PdaPickSettlementService settlementService;
|
||||
@@ -491,6 +497,10 @@ class LoadingPickServiceTest {
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.allPlanItemsPicked(10L)).thenReturn(true);
|
||||
when(repository.approve(20L, 9L, "operator", "checked")).thenReturn(1);
|
||||
var owner = new LoadingPickItemMapper.InboundOwner(101L, 50L);
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(owner));
|
||||
when(repository.inboundForUpdate(101L, 50L)).thenReturn(
|
||||
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
|
||||
|
||||
service.complete(20L, new CompleteLoadingRequest(0, "complete-1", "checked"), 9L, "operator");
|
||||
|
||||
@@ -498,6 +508,35 @@ class LoadingPickServiceTest {
|
||||
verify(loadingListService).finishFromPda(10L, 20L, 9L, "operator");
|
||||
verify(settlementService).settle(20L, 9L, "operator");
|
||||
verify(repository).syncInboundProjections(20L, 9L, "operator");
|
||||
verify(loadingDeliveryService).requirePdaCompletionReady(101L, 50L);
|
||||
verify(loadingDeliveryService).completeLoading(eq(101L), eq(50L), eq(9L), eq("operator"),
|
||||
eq("PDA-complete-1-50"), argThat(value -> value.expectedVersion() == 7
|
||||
&& "checked".equals(value.remark())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void truckBlockerLeavesPdaTaskPlanAndSettlementUntouched() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task);
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
when(repository.review(20L)).thenReturn(null);
|
||||
var owner = new LoadingPickItemMapper.InboundOwner(101L, 50L);
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(owner));
|
||||
when(repository.inboundForUpdate(101L, 50L)).thenReturn(
|
||||
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 7));
|
||||
doThrow(new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,
|
||||
"LOADED门禁未通过:拖车尚未完成(必须为 COMPLETED)"))
|
||||
.when(loadingDeliveryService).requirePdaCompletionReady(101L, 50L);
|
||||
|
||||
assertThatThrownBy(() -> service.complete(20L,
|
||||
new CompleteLoadingRequest(0, "complete-blocked", "checked"), 9L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("拖车尚未完成");
|
||||
|
||||
verify(settlementService, never()).settle(anyLong(), anyLong(), anyString());
|
||||
verify(repository, never()).approve(anyLong(), anyLong(), anyString(), any());
|
||||
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
|
||||
verify(loadingDeliveryService, never()).completeLoading(anyLong(), anyLong(), anyLong(),
|
||||
anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -34,6 +34,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowableOfType;
|
||||
@@ -68,6 +69,8 @@ class SmartLoadingServiceTest {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(loadingPickService.mutationSnapshot(anyLong())).thenReturn(
|
||||
new LoadingPickService.MutationSnapshot(null, null, null, Map.of()));
|
||||
when(cargoClient.detail(anyString(), anyLong(), anyLong(), anyLong())).thenReturn(
|
||||
ApiResponse.success(new OrderCargoClient.CargoBinding(20L, 30L, 1, "货物", 5,
|
||||
new BigDecimal("10"), new BigDecimal("2"))));
|
||||
@@ -95,6 +98,53 @@ class SmartLoadingServiceTest {
|
||||
assertThat(service.draftByBooking(99L).status()).isEqualTo("LOADING");
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesPickedMinimumAndItemCapabilitiesForRunningTask() {
|
||||
LoadingPlanEntity plan = plan("LOADING", 3);
|
||||
LoadingPlanItemEntity line = item(8, 9, 20, 3);
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.plan(1L)).thenReturn(plan);
|
||||
when(repository.items(1L)).thenReturn(List.of(line));
|
||||
when(loadingPickService.mutationSnapshot(1L)).thenReturn(
|
||||
new LoadingPickService.MutationSnapshot(77L, "LOADING", 6, Map.of(8L, 2)));
|
||||
|
||||
Plan result = service.detail(1L);
|
||||
|
||||
assertThat(result.canAssign()).isTrue();
|
||||
assertThat(result.canEdit()).isTrue();
|
||||
assertThat(result.finalCompletionStatus()).isEqualTo("IN_PROGRESS");
|
||||
assertThat(result.pdaTaskId()).isEqualTo(77L);
|
||||
assertThat(result.pdaTaskVersion()).isEqualTo(6);
|
||||
assertThat(result.items()).singleElement().satisfies(item -> {
|
||||
assertThat(item.getPickedPackageCount()).isEqualTo(2);
|
||||
assertThat(item.getMinPackageCount()).isEqualTo(2);
|
||||
assertThat(item.getCanAdjust()).isTrue();
|
||||
assertThat(item.getCanRemove()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedTaskDisablesEveryIncrementalCapability() {
|
||||
LoadingPlanEntity plan = plan("COMPLETED", 4);
|
||||
LoadingPlanItemEntity line = item(8, 9, 20, 3);
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
when(repository.plan(1L)).thenReturn(plan);
|
||||
when(repository.items(1L)).thenReturn(List.of(line));
|
||||
when(loadingPickService.mutationSnapshot(1L)).thenReturn(
|
||||
new LoadingPickService.MutationSnapshot(77L, "COMPLETED", 7, Map.of(8L, 3)));
|
||||
|
||||
Plan result = service.detail(1L);
|
||||
|
||||
assertThat(result.canAssign()).isFalse();
|
||||
assertThat(result.canEdit()).isFalse();
|
||||
assertThat(result.blockedReason()).contains("最终完成");
|
||||
assertThat(result.finalCompletionStatus()).isEqualTo("COMPLETED");
|
||||
assertThat(result.items()).singleElement().satisfies(item -> {
|
||||
assertThat(item.getCanAdjust()).isFalse();
|
||||
assertThat(item.getCanRemove()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotRestoreCompletedPlanInTheLoadingWorkbench() {
|
||||
when(repository.booking(99L)).thenReturn(booking());
|
||||
@@ -227,13 +277,12 @@ class SmartLoadingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void recyclesLegacyLoadingPlanBeforeAddingFirstItem() {
|
||||
LoadingPlanEntity stale = plan("LOADING", 5);
|
||||
LoadingPlanEntity after = plan("EDITING", 7);
|
||||
when(repository.plan(1)).thenReturn(stale, after);
|
||||
void addsItemToLoadingPlanWithoutChangingStableTaskOrPlanStatus() {
|
||||
LoadingPlanEntity loading = plan("LOADING", 5);
|
||||
LoadingPlanEntity after = plan("LOADING", 6);
|
||||
when(repository.plan(1)).thenReturn(loading, after);
|
||||
when(repository.booking(99)).thenReturn(booking());
|
||||
when(repository.recycleUnpicked(1, 5, 7, "u")).thenReturn(1);
|
||||
when(repository.touch(1, 6, 7, "u")).thenReturn(1);
|
||||
when(repository.touch(1, 5, 7, "u")).thenReturn(1);
|
||||
when(repository.items(1)).thenReturn(List.of());
|
||||
when(repository.candidate(100, 10, 20)).thenReturn(candidate(10));
|
||||
when(repository.activeItem(1L, 9L, 20L)).thenReturn(null);
|
||||
@@ -241,11 +290,15 @@ class SmartLoadingServiceTest {
|
||||
new OrderCargoClient.CargoBinding(20L, 30L, 1, "货物", 1,
|
||||
BigDecimal.ONE, BigDecimal.ONE, null, null, null, null, null, null)));
|
||||
|
||||
Plan result = service.assign(1, new AssignRequest(10L, 20L, 1, 5, "assign-recycle"), 7, "u");
|
||||
Plan result = service.assign(1, new AssignRequest(10L, 20L, 1, 5, "assign-loading"), 7, "u");
|
||||
|
||||
assertThat(result.status()).isEqualTo("EDITING");
|
||||
verify(repository).recycleUnpicked(1, 5, 7, "u");
|
||||
verify(repository).touch(1, 6, 7, "u");
|
||||
assertThat(result.status()).isEqualTo("LOADING");
|
||||
var order = inOrder(loadingPickService, repository, loadingModelBridge);
|
||||
order.verify(loadingPickService).lockMutable(1L);
|
||||
order.verify(repository).touch(1, 5, 7, "u");
|
||||
order.verify(loadingModelBridge).bindIncrementalAllocation(1L, 9L, 10L,
|
||||
7L, "u", "assign-loading");
|
||||
order.verify(loadingPickService).sync(eq(1L), any(LoadingPlanItemEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+15
-5
@@ -78,6 +78,7 @@ class TruckAppointmentServiceTest {
|
||||
SaveRequest request = mock(SaveRequest.class);
|
||||
when(request.expectedVersion()).thenReturn(0);
|
||||
when(request.oceanBookingId()).thenReturn(99L);
|
||||
when(request.containerBookingId()).thenReturn(88L);
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setId(99L); booking.setStatus("CONFIRMED");
|
||||
when(oceanBookings.find(99L)).thenReturn(booking);
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", request)).isInstanceOf(BizException.class).hasMessageContaining("放舱");
|
||||
@@ -88,15 +89,24 @@ class TruckAppointmentServiceTest {
|
||||
SaveRequest request = mock(SaveRequest.class);
|
||||
when(request.expectedVersion()).thenReturn(0);
|
||||
when(request.oceanBookingId()).thenReturn(99L);
|
||||
when(request.containerBookingId()).thenReturn(88L);
|
||||
when(request.customerId()).thenReturn(7L);
|
||||
when(request.loadingPlanId()).thenReturn(null);
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setId(99L); booking.setStatus("RELEASED"); booking.setCustomerId(7L); booking.setCustomerName("Canonical Customer");
|
||||
when(oceanBookings.find(99L)).thenReturn(booking);
|
||||
ContainerBookingEntity container = new ContainerBookingEntity(); container.setId(88L);
|
||||
when(containers.find(88L)).thenReturn(container);
|
||||
var allocation = new OceanBookingContainerAllocationEntity(); allocation.setContainerBookingId(88L);
|
||||
when(oceanBookings.allocations(99L)).thenReturn(java.util.List.of(allocation));
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity(); plan.setId(77L); plan.setContainerBookingId(88L);
|
||||
when(loadingPlans.planByBooking(88L)).thenReturn(plan);
|
||||
service.create(7L, "admin", request);
|
||||
ArgumentCaptor<TruckAppointmentEntity> entity = ArgumentCaptor.forClass(TruckAppointmentEntity.class);
|
||||
verify(repository).insert(entity.capture());
|
||||
assertThat(entity.getValue().getCustomerId()).isEqualTo(7L);
|
||||
assertThat(entity.getValue().getCustomerName()).isEqualTo("Canonical Customer");
|
||||
assertThat(entity.getValue().getContainerBookingId()).isEqualTo(88L);
|
||||
assertThat(entity.getValue().getLoadingPlanId()).isEqualTo(77L);
|
||||
verify(repository).history(any(TruckAppointmentHistoryEntity.class));
|
||||
}
|
||||
|
||||
@@ -113,11 +123,11 @@ class TruckAppointmentServiceTest {
|
||||
var allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setContainerBookingId(88L);
|
||||
when(oceanBookings.allocations(99L)).thenReturn(java.util.List.of(allocation));
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity(); plan.setId(77L); plan.setContainerBookingId(66L);
|
||||
when(loadingPlans.plan(77L)).thenReturn(plan);
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity(); plan.setId(78L); plan.setContainerBookingId(88L);
|
||||
when(loadingPlans.planByBooking(88L)).thenReturn(plan);
|
||||
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("loadingPlanId").hasMessageContaining("柜体");
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("loadingPlanId").hasMessageContaining("当前装柜计划");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@@ -134,10 +144,10 @@ class TruckAppointmentServiceTest {
|
||||
var allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setContainerBookingId(88L);
|
||||
when(oceanBookings.allocations(99L)).thenReturn(java.util.List.of(allocation));
|
||||
when(loadingPlans.plan(77L)).thenReturn(null);
|
||||
when(loadingPlans.planByBooking(88L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", request))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("装柜计划不存在");
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("尚未建立装柜计划");
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user