fix(firstleg): resolve loading list lifecycle by plan

This commit is contained in:
2026-08-31 15:26:47 +08:00
parent 0122ad314b
commit c9792b1f63
7 changed files with 137 additions and 5 deletions
@@ -29,6 +29,7 @@ class LoadingListControllerContractTest {
void permissionsMatch() throws Exception {
authority("preloadPage", "platform:firstleg:preload:list", AuthenticatedUser.class, PreloadQuery.class);
authority("preloadDetail", "platform:firstleg:preload:list", AuthenticatedUser.class, long.class, long.class);
authority("lifecycle", "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,
@@ -72,6 +73,8 @@ class LoadingListControllerContractTest {
.contains("校验ISO 6346结构、设备类别和校验位")
.contains("PDA任务尚未领取且无任何物理作业事实")
.contains("/loading-plans/{planId}/confirm")
.contains("/loading-plans/{planId}/lifecycle")
.contains("不得以错误文案或 A0404 猜测阶段")
.contains("confirmationStatus")
.contains("canConfirm")
.contains("blockingReasons")
@@ -116,6 +119,7 @@ class LoadingListControllerContractTest {
assertThat(paths).containsOnlyKeys(
"/api/platform/v1/first-leg/loading-plans",
"/api/platform/v1/first-leg/loading-plans/{planId}",
"/api/platform/v1/first-leg/loading-plans/{planId}/lifecycle",
"/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",
@@ -126,7 +130,11 @@ class LoadingListControllerContractTest {
"LongId", "PreloadPage", "ConfirmedPage", "Customer", "PreloadSummary",
"ConfirmedSummary", "LoadingItem", "OperationHistory", "PreloadDetail",
"ConfirmedDetail", "TransportUpdateRequest", "PreloadReadiness",
"ConfirmPreloadRequest", "PreloadConfirmation", "PreloadConfirmationEnvelope");
"ConfirmPreloadRequest", "PreloadConfirmation", "PreloadConfirmationEnvelope",
"LifecycleResolution", "LifecycleResolutionEnvelope");
assertThat(properties(schemas, "LifecycleResolution")).containsOnlyKeys(
"planId", "containerBookingId", "warehouseId", "planNo", "planStatus",
"detailResource", "requiredPermission");
assertThat(paths)
.doesNotContainKey("/api/platform/v1/first-leg/loading-plans/{planId}/complete");
assertThat(stringMap(schemas.get("LongId"))).containsEntry("type", "string");
@@ -67,6 +67,15 @@ class LoadingListQueryMapperContractTest {
.contains("i.deleted = 0");
}
@Test
void lifecycleAggregateKeepsTheSamePlanIdAcrossEveryStatus() throws Exception {
String aggregate = sql("selectAggregate", Select.class);
assertThat(aggregate)
.contains("WHERE p.id = #{planId}")
.contains("p.plan_status")
.doesNotContain("plan_status = 'COMPLETED'", "plan_status IN ('EDITING', 'LOADING')");
}
@Test
void mutationsShouldUseStatusAndOptimisticVersionConditions() throws Exception {
String touch = sql("touchEditable", Update.class);
@@ -361,7 +361,42 @@ class LoadingListServiceTest {
assertThatThrownBy(() -> service.preloadDetail(10L, 200L))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.NOT_FOUND);
.isEqualTo(ResultCode.FORBIDDEN);
}
@Test
void resolvesStablePlanIdToPreloadOrCompletedLoadingListWithoutGuessingFrom404() {
when(repository.aggregate(10L)).thenReturn(row("LOADING", 2), row("COMPLETED", 3));
var preload = service.lifecycle(10L, 100L);
var completed = service.lifecycle(10L, 100L);
assertThat(preload.planId()).isEqualTo(10L);
assertThat(preload.planStatus()).isEqualTo("LOADING");
assertThat(preload.detailResource()).isEqualTo("PRELOAD");
assertThat(preload.requiredPermission()).isEqualTo("platform:firstleg:preload:list");
assertThat(completed.planId()).isEqualTo(10L);
assertThat(completed.planStatus()).isEqualTo("COMPLETED");
assertThat(completed.detailResource()).isEqualTo("LOADING_LIST");
assertThat(completed.requiredPermission()).isEqualTo("platform:firstleg:loading-list:list");
}
@Test
void stageMismatchIsConflictAndOnlyMissingPlanIsNotFound() {
when(repository.aggregate(10L)).thenReturn(row("COMPLETED", 3));
assertThatThrownBy(() -> service.preloadDetail(10L, 100L))
.isInstanceOfSatisfying(BizException.class, error ->
assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT));
when(repository.aggregate(11L)).thenReturn(row("LOADING", 2));
assertThatThrownBy(() -> service.confirmedDetail(11L, 100L))
.isInstanceOfSatisfying(BizException.class, error ->
assertThat(error.getResultCode()).isEqualTo(ResultCode.CONFLICT));
when(repository.aggregate(12L)).thenReturn(null);
assertThatThrownBy(() -> service.lifecycle(12L, 100L))
.isInstanceOfSatisfying(BizException.class, error ->
assertThat(error.getResultCode()).isEqualTo(ResultCode.NOT_FOUND));
}
@Test