feat: close preload confirmation and loading flow
This commit is contained in:
+22
-2
@@ -5,6 +5,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmPreloadRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentAttachRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentCompleteRequest;
|
||||
import com.baiyue.firstleg.service.LoadingListService;
|
||||
@@ -34,6 +35,8 @@ class LoadingListControllerContractTest {
|
||||
authority("preloadDetail", "platform:firstleg:preload:list", AuthenticatedUser.class, long.class, long.class);
|
||||
authority("updateTransport", "platform:firstleg:preload:edit", AuthenticatedUser.class,
|
||||
long.class, long.class, String.class, TransportUpdateRequest.class);
|
||||
authority("confirmPreload", "platform:firstleg:preload:edit", AuthenticatedUser.class,
|
||||
long.class, String.class, ConfirmPreloadRequest.class);
|
||||
authority("confirmedPage", "platform:firstleg:loading-list:list", AuthenticatedUser.class,
|
||||
ConfirmedQuery.class);
|
||||
authority("confirmedDetail", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
|
||||
@@ -50,6 +53,8 @@ class LoadingListControllerContractTest {
|
||||
void writesAudited() throws Exception {
|
||||
audit("updateTransport", "PRELOAD_TRANSPORT_UPDATE", long.class, long.class, long.class, String.class,
|
||||
String.class, TransportUpdateRequest.class);
|
||||
audit("confirmPreload", "PRELOAD_PLAN_CONFIRM", long.class, long.class, String.class,
|
||||
String.class, ConfirmPreloadRequest.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -74,6 +79,16 @@ class LoadingListControllerContractTest {
|
||||
.contains("beforeSnapshot")
|
||||
.contains("containerNo")
|
||||
.contains("voyageNo")
|
||||
.contains("已推送且仍为 EDITING")
|
||||
.contains("不能清空已创建 PDA 任务的柜号")
|
||||
.contains("/loading-plans/{planId}/confirm")
|
||||
.contains("confirmationStatus")
|
||||
.contains("canConfirm")
|
||||
.contains("blockingReasons")
|
||||
.contains("oceanBookingStatus")
|
||||
.contains("pdaTaskId")
|
||||
.contains("cabinetNo")
|
||||
.doesNotContain("internalContainerNo")
|
||||
.doesNotContain("rollback")
|
||||
.doesNotContain("delete:")
|
||||
.doesNotContain("/items/{itemId}");
|
||||
@@ -114,6 +129,7 @@ class LoadingListControllerContractTest {
|
||||
"/api/platform/v1/first-leg/loading-plans",
|
||||
"/api/platform/v1/first-leg/loading-plans/{planId}",
|
||||
"/api/platform/v1/first-leg/loading-plans/{planId}/transport",
|
||||
"/api/platform/v1/first-leg/loading-plans/{planId}/confirm",
|
||||
"/api/platform/v1/first-leg/loading-lists",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}/customs",
|
||||
@@ -125,7 +141,9 @@ class LoadingListControllerContractTest {
|
||||
assertThat(schemas).containsKeys(
|
||||
"LongId", "PreloadPage", "ConfirmedPage", "Customer", "PreloadSummary",
|
||||
"ConfirmedSummary", "LoadingItem", "OperationHistory", "PreloadDetail",
|
||||
"ConfirmedDetail", "TransportUpdateRequest", "BusinessDocumentEnvelope",
|
||||
"ConfirmedDetail", "TransportUpdateRequest", "PreloadReadiness",
|
||||
"ConfirmPreloadRequest", "PreloadConfirmation", "PreloadConfirmationEnvelope",
|
||||
"BusinessDocumentEnvelope",
|
||||
"DocumentAttachRequest", "DocumentUploadRequest", "DocumentCompleteRequest", "BusinessDocument");
|
||||
assertThat(properties(schemas, "DocumentUploadRequest"))
|
||||
.containsOnlyKeys("file", "expectedVersion", "operationReason");
|
||||
@@ -134,7 +152,9 @@ class LoadingListControllerContractTest {
|
||||
assertThat(stringMap(schemas.get("LongId"))).containsEntry("type", "string");
|
||||
assertThat(properties(schemas, "PreloadPage")).containsKeys("records", "page", "size", "total", "pages");
|
||||
assertThat(properties(schemas, "PreloadSummary"))
|
||||
.containsKeys("planId", "containerBookingId", "customers", "skuCount");
|
||||
.containsKeys("planId", "containerBookingId", "containerNo", "cabinetNo",
|
||||
"customers", "skuCount", "readiness")
|
||||
.doesNotContainKey("internalContainerNo");
|
||||
assertThat(properties(schemas, "ConfirmedSummary"))
|
||||
.containsKeys("planId", "containerBookingId", "loadingListNo");
|
||||
assertThat(properties(schemas, "LoadingItem")).containsKeys(
|
||||
|
||||
+5
-3
@@ -26,11 +26,13 @@ class PlatformSmartLoadingControllerContractTest {
|
||||
authority("delete","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,long.class,DeleteRequest.class);
|
||||
authority("push","platform:firstleg:smart-loading:push",AuthenticatedUser.class,long.class,String.class,PushRequest.class);
|
||||
}
|
||||
@Test void openApiRequiresRealContainerNumberBeforePreloadPush() throws Exception {
|
||||
@Test void openApiPushDoesNotRequireContainerNumberOrCreatePda() 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}/push-preload:")
|
||||
.contains("真实柜号必须已录入")
|
||||
.contains("PDA作业任务");
|
||||
.contains("不要求真实柜号")
|
||||
.contains("不创建 PDA 作业任务")
|
||||
.contains("领取后才进入 LOADING")
|
||||
.doesNotContain("真实柜号必须已录入");
|
||||
}
|
||||
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+"')");}
|
||||
}
|
||||
|
||||
+13
@@ -166,6 +166,19 @@ class LoadingModelBridgeServiceTest {
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmationGateRejectsIncompleteAllocatedLinks() {
|
||||
when(repository.loadingPlan(40)).thenReturn(new LoadingPlanSnapshot(
|
||||
40L, 50L, "C1", "EDITING", 3, LocalDateTime.now()));
|
||||
when(repository.allocatedScopes(40)).thenReturn(List.of(
|
||||
new AllocationScope(10L, 20L, 8), new AllocationScope(11L, 21L, 4)));
|
||||
when(repository.byLoadingPlan(40)).thenReturn(List.of(
|
||||
link("PENDING", 2, "EDITING", 3, 8)));
|
||||
|
||||
assertThatThrownBy(() -> service.requireAllocatedLinksForConfirmation(40))
|
||||
.hasMessageContaining("关联不完整");
|
||||
}
|
||||
|
||||
@Test
|
||||
void terminalGateRequiresEveryLinkedContainerPlanCompleted() {
|
||||
when(repository.byInbound(10, 20)).thenReturn(List.of(
|
||||
|
||||
+20
-1
@@ -37,6 +37,10 @@ class LoadingListQueryMapperContractTest {
|
||||
.contains("p.preload_pushed_at IS NOT NULL")
|
||||
.contains("p.plan_status IN ('EDITING', 'LOADING')")
|
||||
.contains("active_item.deleted = 0")
|
||||
.contains("fl_preload_loading_plan_link")
|
||||
.contains("fl_ocean_booking_container_allocation")
|
||||
.contains("fl_loading_pick_task")
|
||||
.contains("pda_task_status")
|
||||
.contains("customer_item.account_id = #{query.customerId}")
|
||||
.contains("b.warehouse_id = #{query.warehouseId}")
|
||||
.doesNotContain("query.accountId")
|
||||
@@ -54,14 +58,19 @@ class LoadingListQueryMapperContractTest {
|
||||
void mutationsShouldUseStatusAndOptimisticVersionConditions() throws Exception {
|
||||
String touch = sql("touchEditable", Update.class);
|
||||
String transport = sql("updateTransport", Update.class);
|
||||
String confirmPreload = sql("confirmLinkedPreload", Update.class);
|
||||
String confirmPlan = sql("touchForPreloadConfirmation", Update.class);
|
||||
String advance = sql("advance", Update.class);
|
||||
|
||||
assertThat(touch)
|
||||
.contains("plan_status = 'LOADING'")
|
||||
.contains("plan_status IN ('EDITING', 'LOADING')")
|
||||
.contains("preload_pushed_at IS NOT NULL")
|
||||
.contains("version = #{expectedVersion}")
|
||||
.contains("version = version + 1");
|
||||
assertThat(transport)
|
||||
.contains("SET b.container_no = #{containerNo}, b.voyage_no = #{voyageNo}")
|
||||
.contains("plan_status IN ('EDITING', 'LOADING')")
|
||||
.contains("preload_pushed_at IS NOT NULL")
|
||||
.contains("#{containerNo} IS NOT NULL")
|
||||
.contains("fl_loading_pick_task")
|
||||
.doesNotContain("DELETE")
|
||||
@@ -71,6 +80,16 @@ class LoadingListQueryMapperContractTest {
|
||||
.contains("plan_status = #{toStatus}")
|
||||
.contains("version = #{expectedVersion}")
|
||||
.doesNotContain("ROLLED_BACK");
|
||||
assertThat(confirmPreload)
|
||||
.contains("status = 'CONFIRMED'")
|
||||
.contains("status = #{fromStatus}")
|
||||
.contains("version = #{expectedVersion}")
|
||||
.contains("version = version + 1");
|
||||
assertThat(confirmPlan)
|
||||
.contains("plan_status = 'EDITING'")
|
||||
.contains("preload_pushed_at IS NOT NULL")
|
||||
.contains("version = #{expectedVersion}")
|
||||
.contains("version = version + 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -39,6 +39,13 @@ class LoadingListRepositoryTest {
|
||||
row.setPlanNo("LP-41");
|
||||
row.setPlanStatus("LOADING");
|
||||
row.setVersion(1);
|
||||
row.setContainerNo("MSCU-001");
|
||||
row.setCabinetNo("CAB-001");
|
||||
row.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
row.setPreloadLinkCount(2L);
|
||||
row.setConfirmedPreloadCount(1L);
|
||||
row.setOceanBookingId(81L);
|
||||
row.setOceanBookingStatus("RELEASED");
|
||||
|
||||
Page<AggregateRow> preloadPage = new Page<>(1, 20);
|
||||
preloadPage.setRecords(List.of(row));
|
||||
@@ -58,6 +65,10 @@ class LoadingListRepositoryTest {
|
||||
|
||||
assertThat(preload.planId()).isEqualTo(41L);
|
||||
assertThat(preload.containerBookingId()).isEqualTo(61L);
|
||||
assertThat(preload.containerNo()).isEqualTo("MSCU-001");
|
||||
assertThat(preload.cabinetNo()).isEqualTo("CAB-001");
|
||||
assertThat(preload.readiness().confirmationStatus()).isEqualTo("PARTIALLY_CONFIRMED");
|
||||
assertThat(preload.readiness().oceanBookingStatus()).isEqualTo("RELEASED");
|
||||
assertThat(confirmed.planId()).isEqualTo(41L);
|
||||
assertThat(confirmed.containerBookingId()).isEqualTo(61L);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmPreloadRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadConfirmationResponse;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadReadiness;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentAttachRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentCompleteRequest;
|
||||
import com.baiyue.firstleg.client.LoadingDocumentFileClient;
|
||||
@@ -13,6 +16,7 @@ import com.baiyue.firstleg.entity.ContainerDocumentEntity;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.HistoryRow;
|
||||
import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
|
||||
import com.baiyue.firstleg.repository.LoadingListRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -31,6 +35,7 @@ import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -45,6 +50,8 @@ class LoadingListServiceTest {
|
||||
@MockBean
|
||||
private LoadingPickRepository pickRepository;
|
||||
@MockBean
|
||||
private LoadingPickService loadingPickService;
|
||||
@MockBean
|
||||
private LoadingDocumentFileClient documentFiles;
|
||||
@MockBean
|
||||
private LoadingCompletedOutboxService loadingCompletedOutboxService;
|
||||
@@ -87,11 +94,43 @@ class LoadingListServiceTest {
|
||||
new TransportUpdateRequest(" CSLU1234567 ", "023E", 3, "补录箱号"));
|
||||
|
||||
verify(repository).updateTransport(10L, 3, "CSLU1234567", "023E", 9L, "operator");
|
||||
verify(loadingPickService).beginIfReady(30L, 9L, "operator");
|
||||
verify(repository).appendHistory(eq(10L), anyString(), eq("UPDATE_PRELOAD_TRANSPORT"),
|
||||
eq("LOADING"), eq("LOADING"), anyString(), anyString(), eq(9L),
|
||||
eq("operator"), eq("补录箱号"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushedEditingPreloadCanReceiveContainerNumber() {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
AggregateRow after = row("EDITING", 4);
|
||||
after.setPreloadPushedAt(before.getPreloadPushedAt());
|
||||
after.setContainerNo("CSLU1234567");
|
||||
when(repository.aggregate(10L)).thenReturn(before, after);
|
||||
when(repository.updateTransport(10L, 3, "CSLU1234567", null, 9L, "operator")).thenReturn(1);
|
||||
when(repository.preloadDetail(after)).thenReturn(
|
||||
new com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail(null, List.of(), List.of()));
|
||||
|
||||
service.updateTransport(10L, 100L, 9L, "operator", "editing-key",
|
||||
new TransportUpdateRequest("CSLU1234567", null, 3, "补录柜号"));
|
||||
|
||||
verify(loadingPickService).beginIfReady(30L, 9L, "operator");
|
||||
}
|
||||
|
||||
@Test
|
||||
void editingPlanCannotReceiveTransportBeforePush() {
|
||||
when(repository.aggregate(10L)).thenReturn(row("EDITING", 3));
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "editing-key",
|
||||
new TransportUpdateRequest("CSLU1234567", null, 3, "补录柜号")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已推送");
|
||||
|
||||
verify(repository, never()).updateTransport(eq(10L), eq(3), anyString(),
|
||||
org.mockito.ArgumentMatchers.isNull(), eq(9L), anyString());
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectTransportUpdateAfterConfirmation() {
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
@@ -456,10 +495,110 @@ class LoadingListServiceTest {
|
||||
verify(repository, never()).aggregateByBooking(30L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmsEveryLinkedPreloadAndCreatesSinglePdaTaskAtomically() {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
AggregateRow after = row("EDITING", 4);
|
||||
after.setPreloadPushedAt(before.getPreloadPushedAt());
|
||||
List<Link> links = List.of(
|
||||
new Link(1L, 101L, 201L, 301L, "PENDING", 2, 10L, 30L,
|
||||
"MSCU-001", "EDITING", 3, 4, null),
|
||||
new Link(2L, 102L, 202L, 302L, "CONFIRMING", 5, 10L, 30L,
|
||||
"MSCU-001", "EDITING", 3, 6, null));
|
||||
PreloadReadiness readiness = readiness("CONFIRMED", false, true, 80L);
|
||||
var detail = org.mockito.Mockito.mock(
|
||||
com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail.class);
|
||||
var summary = org.mockito.Mockito.mock(
|
||||
com.baiyue.firstleg.dto.LoadingListDtos.PreloadSummary.class);
|
||||
when(detail.summary()).thenReturn(summary);
|
||||
when(summary.readiness()).thenReturn(readiness);
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
when(repository.aggregate(10L)).thenReturn(before, after);
|
||||
when(loadingPickService.requirePreloadConfirmationReady(10L)).thenReturn(links);
|
||||
when(repository.touchForPreloadConfirmation(10L, 3, 9L, "operator")).thenReturn(1);
|
||||
when(repository.confirmLinkedPreload(301L, "PENDING", 2, 9L, "operator")).thenReturn(1);
|
||||
when(repository.confirmLinkedPreload(302L, "CONFIRMING", 5, 9L, "operator")).thenReturn(1);
|
||||
when(repository.preloadDetail(after)).thenReturn(detail);
|
||||
|
||||
PreloadConfirmationResponse response = service.confirmPreload(
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3));
|
||||
|
||||
assertThat(response.confirmationStatus()).isEqualTo("CONFIRMED");
|
||||
assertThat(response.version()).isEqualTo(4);
|
||||
assertThat(response.readiness().pdaTaskId()).isEqualTo(80L);
|
||||
verify(repository).confirmLinkedPreload(301L, "PENDING", 2, 9L, "operator");
|
||||
verify(repository).confirmLinkedPreload(302L, "CONFIRMING", 5, 9L, "operator");
|
||||
verify(loadingPickService, times(1)).beginIfReady(30L, 9L, "operator");
|
||||
verify(repository).appendHistory(eq(10L), anyString(), eq("CONFIRM_PRELOAD"),
|
||||
eq("EDITING"), eq("EDITING"), anyString(), anyString(), eq(9L),
|
||||
eq("operator"), org.mockito.ArgumentMatchers.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exactConfirmationReplayReturnsFrozenResponseWithoutTouchingPlan() throws Exception {
|
||||
PreloadConfirmationResponse expected = new PreloadConfirmationResponse(
|
||||
10L, 30L, "CONFIRMED", 4, readiness("CONFIRMED", false, true, 80L));
|
||||
String requestHash = sha256("CONFIRM_PRELOAD|10|3");
|
||||
HistoryRow replay = new HistoryRow();
|
||||
replay.setAfterSnapshot(new ObjectMapper().writeValueAsString(
|
||||
java.util.Map.of("requestHash", requestHash, "response", expected)));
|
||||
when(repository.history(anyString())).thenReturn(replay);
|
||||
|
||||
PreloadConfirmationResponse actual = service.confirmPreload(
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3));
|
||||
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
verify(loadingPickService, never()).requirePreloadConfirmationReady(anyLong());
|
||||
verify(repository, never()).touchForPreloadConfirmation(anyLong(),
|
||||
org.mockito.ArgumentMatchers.anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmationGateFailureLeavesPlanAndEveryPreloadUntouched() {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
when(loadingPickService.requirePreloadConfirmationReady(10L))
|
||||
.thenThrow(new BizException(ResultCode.CONFLICT, "第二票订舱尚未放舱"));
|
||||
|
||||
assertThatThrownBy(() -> service.confirmPreload(
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(3)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("第二票");
|
||||
|
||||
verify(repository, never()).touchForPreloadConfirmation(anyLong(),
|
||||
org.mockito.ArgumentMatchers.anyInt(), anyLong(), anyString());
|
||||
verify(repository, never()).confirmLinkedPreload(anyLong(), anyString(),
|
||||
org.mockito.ArgumentMatchers.anyInt(), anyLong(), anyString());
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reusedConfirmationKeyWithDifferentVersionConflictsBeforeReadiness() {
|
||||
HistoryRow replay = new HistoryRow();
|
||||
replay.setAfterSnapshot("{\"requestHash\":\"different\"}");
|
||||
when(repository.history(anyString())).thenReturn(replay);
|
||||
|
||||
assertThatThrownBy(() -> service.confirmPreload(
|
||||
10L, 9L, "operator", "confirm-key", new ConfirmPreloadRequest(4)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不同请求");
|
||||
|
||||
verify(loadingPickService, never()).requirePreloadConfirmationReady(anyLong());
|
||||
}
|
||||
|
||||
private static PreloadReadiness readiness(String confirmationStatus, boolean canConfirm,
|
||||
boolean readyForPda, Long taskId) {
|
||||
return new PreloadReadiness(true, true, 2, 2, 90L, "RELEASED", taskId,
|
||||
taskId == null ? null : "LOADING", confirmationStatus, canConfirm,
|
||||
readyForPda, List.of());
|
||||
}
|
||||
|
||||
private static AggregateRow row(String status, int version) {
|
||||
AggregateRow row = new AggregateRow();
|
||||
row.setPlanId(10L);
|
||||
row.setPlanNo("PL-10");
|
||||
row.setContainerBookingId(30L);
|
||||
row.setPlanStatus(status);
|
||||
row.setWarehouseId(100L);
|
||||
row.setVersion(version);
|
||||
|
||||
@@ -19,6 +19,8 @@ import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
||||
import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
|
||||
import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
@@ -38,6 +40,7 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
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.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -55,6 +58,8 @@ class LoadingPickServiceTest {
|
||||
@MockBean
|
||||
private OceanBookingRepository oceanBookingRepository;
|
||||
@MockBean
|
||||
private LoadingModelBridgeService loadingModelBridge;
|
||||
@MockBean
|
||||
private LoadingListService loadingListService;
|
||||
@MockBean
|
||||
private InboundStatusHistoryMapper inboundHistories;
|
||||
@@ -77,6 +82,8 @@ class LoadingPickServiceTest {
|
||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
|
||||
when(smartLoadingRepository.plan(10L)).thenReturn(plan);
|
||||
when(loadingModelBridge.byLoadingPlan(10L)).thenReturn(List.of(
|
||||
new Link(1L, 1L, 50L, 2L, "CONFIRMED", 1, 10L, 40L, "MSCU-001", "EDITING", 1, 1, null)));
|
||||
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
|
||||
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
|
||||
when(repository.taskByPlan(10L)).thenReturn(null);
|
||||
@@ -102,6 +109,8 @@ class LoadingPickServiceTest {
|
||||
var ocean = new OceanBookingEntity(); ocean.setId(90L); ocean.setStatus("CONFIRMED");
|
||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
|
||||
when(loadingModelBridge.byLoadingPlan(10L)).thenReturn(List.of(
|
||||
new Link(1L, 1L, 50L, 2L, "CONFIRMED", 1, 10L, 40L, "MSCU-001", "EDITING", 1, 1, null)));
|
||||
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
|
||||
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
|
||||
|
||||
@@ -110,6 +119,93 @@ class LoadingPickServiceTest {
|
||||
verify(repository, never()).insert(any(LoadingPickTaskEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotOpenTaskBeforeEveryLinkedPreloadIsConfirmed() {
|
||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
booking.setId(40L); booking.setContainerNo("MSCU-001");
|
||||
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
|
||||
plan.setId(10L); plan.setContainerBookingId(40L); plan.setPlanStatus("EDITING");
|
||||
plan.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
|
||||
when(loadingModelBridge.byLoadingPlan(10L)).thenReturn(List.of(
|
||||
new Link(1L, 1L, 50L, 2L, "PENDING", 1, 10L, 40L, "MSCU-001", "EDITING", 1, 1, null)));
|
||||
|
||||
service.beginIfReady(40L, 7L, "admin");
|
||||
|
||||
verify(repository, never()).insert(any(LoadingPickTaskEntity.class));
|
||||
verify(oceanBookingRepository, never()).allocationByContainer(40L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void planConfirmationGateRequiresRealContainerAndReleasedBooking() {
|
||||
Link link = new Link(1L, 1L, 50L, 2L, "PENDING", 1,
|
||||
10L, 40L, "MSCU-001", "EDITING", 3, 2, null);
|
||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
booking.setId(40L); booking.setContainerNo("MSCU-001");
|
||||
var allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setOceanBookingId(90L);
|
||||
var ocean = new OceanBookingEntity();
|
||||
ocean.setId(90L); ocean.setStatus("RELEASED");
|
||||
when(loadingModelBridge.requireAllocatedLinksForConfirmation(10L)).thenReturn(List.of(link));
|
||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
|
||||
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
|
||||
|
||||
assertThat(service.requirePreloadConfirmationReady(10L)).containsExactly(link);
|
||||
}
|
||||
|
||||
@Test
|
||||
void planConfirmationGateRejectsMissingRealContainerNumber() {
|
||||
Link link = new Link(1L, 1L, 50L, 2L, "PENDING", 1,
|
||||
10L, 40L, null, "EDITING", 3, 2, null);
|
||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
booking.setId(40L);
|
||||
when(loadingModelBridge.requireAllocatedLinksForConfirmation(10L)).thenReturn(List.of(link));
|
||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||
|
||||
assertThatThrownBy(() -> service.requirePreloadConfirmationReady(10L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("真实集装箱号");
|
||||
verify(oceanBookingRepository, never()).allocationByContainer(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void newKeyCannotConfirmAnAlreadyConfirmedPlanAgain() {
|
||||
Link confirmed = new Link(1L, 1L, 50L, 2L, "CONFIRMED", 2,
|
||||
10L, 40L, "MSCU-001", "EDITING", 4, 2, null);
|
||||
when(loadingModelBridge.requireAllocatedLinksForConfirmation(10L))
|
||||
.thenReturn(List.of(confirmed));
|
||||
|
||||
assertThatThrownBy(() -> service.requirePreloadConfirmationReady(10L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已确认");
|
||||
verify(smartLoadingRepository, never()).booking(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readinessCreatesAtMostOneTask() {
|
||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
booking.setId(40L); booking.setContainerNo("MSCU-001");
|
||||
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
|
||||
plan.setId(10L); plan.setContainerBookingId(40L); plan.setPlanStatus("EDITING");
|
||||
plan.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
var allocation = new OceanBookingContainerAllocationEntity(); allocation.setOceanBookingId(90L);
|
||||
var ocean = new OceanBookingEntity(); ocean.setId(90L); ocean.setStatus("RELEASED");
|
||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
|
||||
when(loadingModelBridge.byLoadingPlan(10L)).thenReturn(List.of(
|
||||
new Link(1L, 1L, 50L, 2L, "CONFIRMED", 1, 10L, 40L, "MSCU-001", "EDITING", 1, 1, null)));
|
||||
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
|
||||
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
|
||||
when(repository.taskByPlan(10L)).thenReturn(new LoadingPickTaskEntity());
|
||||
|
||||
service.beginIfReady(40L, 7L, "admin");
|
||||
|
||||
verify(repository, never()).insert(any(LoadingPickTaskEntity.class));
|
||||
verify(smartLoadingRepository, never()).startPicking(
|
||||
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(),
|
||||
org.mockito.ArgumentMatchers.anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void stableCabinetAndPlanQueriesReturnSameTaskWithCurrentContainerNumber() {
|
||||
LoadingPickTaskEntity task = task();
|
||||
|
||||
@@ -397,10 +397,10 @@ class SmartLoadingServiceTest {
|
||||
assertThat(history.getValue().getActionType()).isEqualTo("PUSH_PRELOAD");
|
||||
assertThat(history.getValue().getAfterSnapshot())
|
||||
.contains(sha256("PUSH_PRELOAD|1|3"));
|
||||
var ordered = inOrder(splitPreloadService, loadingModelBridge, loadingPickService);
|
||||
var ordered = inOrder(splitPreloadService, loadingModelBridge);
|
||||
ordered.verify(splitPreloadService).ensurePendingForLoadingAllocation(9L, 10L, 7L, "u");
|
||||
ordered.verify(loadingModelBridge).bindAllocatedBeforeLoading(1, 7, "u", "key");
|
||||
ordered.verify(loadingPickService).beginIfReady(99, 7, "u");
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -539,37 +539,34 @@ class SmartLoadingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsPushWithoutContainerNumberBeforePdaOpening() {
|
||||
void pushesWithoutContainerNumberAndDoesNotOpenPda() {
|
||||
LoadingPlanEntity editing = plan("EDITING", 1);
|
||||
ContainerBookingEntity booking = booking();
|
||||
booking.setContainerNo(null);
|
||||
when(repository.plan(1)).thenReturn(editing);
|
||||
when(repository.plan(1)).thenReturn(editing, plan("EDITING", 2));
|
||||
when(repository.booking(99)).thenReturn(booking);
|
||||
when(repository.items(1)).thenReturn(List.of(item(8, 9, 20, 1)));
|
||||
when(repository.push(1, "no-container-key", 1, 7, "u")).thenReturn(1);
|
||||
|
||||
BizException error = catchThrowableOfType(
|
||||
() -> service.pushPreload(1, "no-container-key", new PushRequest(1), 7, "u"),
|
||||
BizException.class);
|
||||
service.pushPreload(1, "no-container-key", new PushRequest(1), 7, "u");
|
||||
|
||||
assertThat(error.getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||
assertThat(error.getMessage()).contains("真实柜号");
|
||||
verify(repository, never()).push(anyLong(), anyString(), anyInt(), anyLong(), anyString());
|
||||
verify(repository).push(1, "no-container-key", 1, 7, "u");
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReplayWhenTheContainerNumberIsStillMissing() {
|
||||
void exactReplayWhenContainerNumberIsStillMissingIsReadOnly() {
|
||||
LoadingPlanEntity pushed = plan("EDITING", 2);
|
||||
pushed.setPushIdempotencyKey("replay-key");
|
||||
ContainerBookingEntity booking = booking();
|
||||
booking.setContainerNo(" ");
|
||||
when(repository.plan(1)).thenReturn(pushed);
|
||||
when(repository.booking(99)).thenReturn(booking);
|
||||
when(repository.operation("replay-key"))
|
||||
.thenReturn(history(1, null, "PUSH_PRELOAD", sha256("PUSH_PRELOAD|1|2")));
|
||||
|
||||
BizException error = catchThrowableOfType(
|
||||
() -> service.pushPreload(1, "replay-key", new PushRequest(2), 7, "u"),
|
||||
BizException.class);
|
||||
service.pushPreload(1, "replay-key", new PushRequest(2), 7, "u");
|
||||
|
||||
assertThat(error.getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
|
||||
verify(repository, never()).push(anyLong(), anyString(), anyInt(), anyLong(), anyString());
|
||||
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
+7
-3
@@ -1,6 +1,5 @@
|
||||
package com.baiyue.firstleg.split.controller;
|
||||
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.ConfirmPreloadRequest;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.SmartSplitRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -13,14 +12,19 @@ class PlatformSplitPreloadControllerContractTest {
|
||||
@Test
|
||||
void shouldDeclareStableOperationsAndButtonPermissions() throws Exception {
|
||||
assertAuthority("smart", "hasAuthority('firstleg:inbound:split')", SmartSplitRequest.class);
|
||||
assertAuthority("confirming", "hasAuthority('firstleg:inbound:preload')", ConfirmPreloadRequest.class);
|
||||
assertAuthority("confirm", "hasAuthority('firstleg:inbound:preload')", ConfirmPreloadRequest.class);
|
||||
assertThat(PlatformSplitPreloadController.class
|
||||
.getDeclaredMethod("detail", com.baiyue.common.security.AuthenticatedUser.class,
|
||||
long.class, long.class).getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('firstleg:inbound:preload-view')");
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotExposeInboundScopedPreloadConfirmation() {
|
||||
assertThat(PlatformSplitPreloadController.class.getDeclaredMethods())
|
||||
.noneMatch(method -> method.getName().equals("confirm")
|
||||
|| method.getName().equals("confirming"));
|
||||
}
|
||||
|
||||
private void assertAuthority(String name, String expected, Class<?> requestType) throws Exception {
|
||||
Method method = PlatformSplitPreloadController.class.getDeclaredMethod(name,
|
||||
com.baiyue.common.security.AuthenticatedUser.class, long.class, long.class,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package com.baiyue.firstleg.split.service;
|
||||
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import com.baiyue.firstleg.loading.bridge.service.LoadingModelBridgeService;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.ConfirmPreloadRequest;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.PreloadItem;
|
||||
import com.baiyue.firstleg.split.dto.SplitRequests.SmartSplitRequest;
|
||||
import com.baiyue.firstleg.split.dto.SplitResponses.SplitActionResponse;
|
||||
@@ -33,8 +30,6 @@ import static org.mockito.Mockito.never;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class SplitPreloadServiceTest {
|
||||
@Mock SplitRepository repository;
|
||||
@Mock LoadingModelBridgeService loadingModelBridge;
|
||||
@Mock InboundStatusHistoryMapper histories;
|
||||
@InjectMocks SplitPreloadService service;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@@ -85,53 +80,4 @@ class SplitPreloadServiceTest {
|
||||
verify(repository, never()).createPlan(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmsPlanAndMovesInboundToLoading() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "WAIT_SPLIT", 4));
|
||||
PreloadPlanEntity plan = new PreloadPlanEntity();
|
||||
plan.setTenantId(10L); plan.setInboundId(20L); plan.setStatus("CONFIRMING"); plan.setVersion(1);
|
||||
plan.setItemsJson("[]");
|
||||
when(repository.findPlan(10, 20)).thenReturn(plan);
|
||||
org.mockito.Mockito.doNothing().when(loadingModelBridge).requireStarted(10, 20);
|
||||
when(repository.changePlanStatus(10, 20, "CONFIRMING", "CONFIRMED", 7, "admin", 1)).thenReturn(1);
|
||||
when(repository.beginLoading(10, 20, 7, 4)).thenReturn(1);
|
||||
SplitActionResponse result = service.confirmPreload(10, 20, 7, "admin", "k3",
|
||||
new ConfirmPreloadRequest(1));
|
||||
assertThat(result.mainStatus()).isEqualTo("LOADING");
|
||||
verify(loadingModelBridge).requireStarted(10, 20);
|
||||
verify(repository).beginLoading(10, 20, 7, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void startsPreloadConfirmationAfterLoadingBeginsWithLinkedContainerGate() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "LOADING", 5));
|
||||
PreloadPlanEntity plan = new PreloadPlanEntity();
|
||||
plan.setTenantId(10L); plan.setInboundId(20L); plan.setStatus("PENDING"); plan.setVersion(3);
|
||||
plan.setItemsJson("[]");
|
||||
when(repository.findPlan(10, 20)).thenReturn(plan);
|
||||
org.mockito.Mockito.doNothing().when(loadingModelBridge).requireStarted(10, 20);
|
||||
when(repository.changePlanStatus(10, 20, "PENDING", "CONFIRMING", 7, "admin", 3)).thenReturn(1);
|
||||
|
||||
SplitActionResponse result = service.startConfirming(10, 20, 7, "admin", "k4",
|
||||
new ConfirmPreloadRequest(3));
|
||||
|
||||
assertThat(result.mainStatus()).isEqualTo("LOADING");
|
||||
verify(loadingModelBridge).requireStarted(10, 20);
|
||||
verify(repository).changePlanStatus(10, 20, "PENDING", "CONFIRMING", 7, "admin", 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsLoadingPreloadConfirmationWhenLinkedContainersHaveNotStarted() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "LOADING", 5));
|
||||
org.mockito.Mockito.doThrow(new IllegalStateException("not started"))
|
||||
.when(loadingModelBridge).requireStarted(10, 20);
|
||||
|
||||
assertThatThrownBy(() -> service.startConfirming(10, 20, 7, "admin", "k5",
|
||||
new ConfirmPreloadRequest(3)))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("not started");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user