优化
This commit is contained in:
+2
-2
@@ -72,7 +72,7 @@ public class PlatformLoadingListController {
|
||||
return ApiResponse.success(service.lifecycle(planId, warehouseId));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改装柜运输字段", description = "已推送且仍为 EDITING 的预装清单可补录或修改集装箱号与航次;LOADING 阶段按稳定柜次身份规则维护,不能清空已创建 PDA 任务的柜号。")
|
||||
@Operation(summary = "修改装柜运输字段", description = "已推送且仍为 EDITING、PDA尚未领取或产生物理作业事实时,可首次补录或纠正集装箱号并维护航次;已有柜号的纠正必须提供原因和版本,柜号不能清空。")
|
||||
@PatchMapping("/loading-plans/{planId}/transport")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
|
||||
public ApiResponse<PreloadDetail> updateTransport(
|
||||
@@ -86,7 +86,7 @@ public class PlatformLoadingListController {
|
||||
planId, warehouseId, user.userId(), user.username(), idempotencyKey, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "确认整份预装计划", description = "按 loadingPlanId 原子确认全部关联入库预装票据;要求已推送、真实集装箱号已录入、关联订舱已放舱。成功后唯一创建待领取 PDA 任务,claim 后才进入 LOADING。")
|
||||
@Operation(summary = "确认整份预装计划", description = "按 loadingPlanId 原子确认全部关联入库预装票据;要求已推送且关联订舱已放舱,真实集装箱号可在PDA确认装柜完成前补录。成功后唯一创建待领取 PDA 任务,claim 后才进入 LOADING。")
|
||||
@PostMapping("/loading-plans/{planId}/confirm")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
|
||||
public ApiResponse<PreloadConfirmationResponse> confirmPreload(
|
||||
|
||||
@@ -51,7 +51,7 @@ public final class LoadingListDtos {
|
||||
@Size(max = 64) String containerNo,
|
||||
@Size(max = 64) String voyageNo,
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max = 500) String operationReason
|
||||
@Size(max = 500) String operationReason
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -250,7 +250,6 @@ public interface LoadingListQueryMapper {
|
||||
b.updated_by = #{operatorId}, b.updated_by_name = #{operatorName},
|
||||
b.updated_at = CURRENT_TIMESTAMP(3), b.version = b.version + 1
|
||||
WHERE p.id = #{planId} AND p.plan_status='EDITING' AND p.preload_pushed_at IS NOT NULL
|
||||
AND #{containerNo} IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM fl_loading_pick_task task
|
||||
JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id=task.id
|
||||
WHERE task.loading_plan_id=p.id)
|
||||
|
||||
@@ -151,13 +151,12 @@ public class LoadingListRepository {
|
||||
java.util.ArrayList<String> blockers = new java.util.ArrayList<>();
|
||||
if (!pushed) blockers.add("PRELOAD_NOT_PUSHED");
|
||||
if (!editing) blockers.add("LOADING_PLAN_NOT_EDITING");
|
||||
if (!containerReady) blockers.add("CONTAINER_NO_MISSING");
|
||||
if (linkCount == 0) blockers.add("PRELOAD_LINK_MISSING");
|
||||
if (!oceanLinked) blockers.add("OCEAN_BOOKING_NOT_LINKED");
|
||||
else if (!released) blockers.add("OCEAN_BOOKING_NOT_RELEASED");
|
||||
if (taskCreated) blockers.add("PDA_TASK_ALREADY_CREATED");
|
||||
boolean allConfirmed = linkCount > 0 && confirmedCount == linkCount;
|
||||
boolean prerequisitesReady = pushed && editing && containerReady && linkCount > 0 && released;
|
||||
boolean prerequisitesReady = pushed && editing && linkCount > 0 && released;
|
||||
boolean canConfirm = prerequisitesReady && !taskCreated && !allConfirmed;
|
||||
if (allConfirmed) blockers.add("PRELOAD_ALREADY_CONFIRMED");
|
||||
String confirmationStatus = allConfirmed ? "CONFIRMED"
|
||||
|
||||
@@ -423,7 +423,6 @@ public class LoadingListService {
|
||||
AggregateRow before = requirePlan(planId);
|
||||
requireWarehouse(before, warehouseId);
|
||||
String reason = trim(request.operationReason());
|
||||
if (!StringUtils.hasText(reason)) throw badRequest("纠错原因不能为空");
|
||||
String operationId = operationId("TRANSPORT", planId, idempotencyKey);
|
||||
HistoryRow replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
@@ -434,15 +433,21 @@ public class LoadingListService {
|
||||
return repository.preloadDetail(before);
|
||||
}
|
||||
|
||||
requireTransportCorrectionMutable(before);
|
||||
requireTransportMutable(before);
|
||||
String containerNo = targetContainerNo(before, request.containerNo());
|
||||
String voyageNo = nullableTrim(request.voyageNo());
|
||||
boolean correctingContainerNo = StringUtils.hasText(before.getContainerNo())
|
||||
&& !java.util.Objects.equals(before.getContainerNo(), containerNo);
|
||||
if (correctingContainerNo && !StringUtils.hasText(reason)) {
|
||||
throw badRequest("纠正已有真实集装箱号时纠错原因不能为空");
|
||||
}
|
||||
String requestHash = transportHash(planId, request.expectedVersion(), containerNo, voyageNo, reason);
|
||||
if (java.util.Objects.equals(before.getContainerNo(), containerNo)
|
||||
&& java.util.Objects.equals(before.getVoyageNo(), voyageNo)) {
|
||||
return repository.preloadDetail(before);
|
||||
}
|
||||
if (containerRepository.containerNoExists(containerNo, before.getContainerBookingId())) {
|
||||
if (containerNo != null
|
||||
&& containerRepository.containerNoExists(containerNo, before.getContainerBookingId())) {
|
||||
throw conflict("集装箱号已存在");
|
||||
}
|
||||
try {
|
||||
@@ -464,8 +469,9 @@ public class LoadingListService {
|
||||
}
|
||||
|
||||
private static String targetContainerNo(AggregateRow before, String requested) {
|
||||
return requested == null ? Iso6346ContainerNumber.require(before.getContainerNo())
|
||||
: Iso6346ContainerNumber.require(requested);
|
||||
if (requested != null) return Iso6346ContainerNumber.require(requested);
|
||||
return StringUtils.hasText(before.getContainerNo())
|
||||
? Iso6346ContainerNumber.require(before.getContainerNo()) : null;
|
||||
}
|
||||
|
||||
private static String transportHash(long planId, int version, String containerNo,
|
||||
@@ -577,14 +583,11 @@ public class LoadingListService {
|
||||
}
|
||||
}
|
||||
|
||||
private void requireTransportCorrectionMutable(AggregateRow row) {
|
||||
private void requireTransportMutable(AggregateRow row) {
|
||||
if (!"EDITING".equals(row.getPlanStatus())) {
|
||||
throw conflict("仅已推送且PDA尚未开始的预装计划可纠错真实集装箱号");
|
||||
}
|
||||
if (row.getPreloadPushedAt() == null) throw conflict("仅已推送的预装清单可维护运输字段");
|
||||
if (!StringUtils.hasText(row.getContainerNo())) {
|
||||
throw conflict("真实集装箱号只能在关联已放舱订舱时首次写入");
|
||||
}
|
||||
if (repository.hasPdaWorkStarted(row.getPlanId())) {
|
||||
throw conflict("PDA任务已领取、已开始作业或已有物理作业事实,不能修改真实集装箱号");
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.InventoryResource;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.InboundInventory;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.InventorySnapshot;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PendingTask;
|
||||
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
|
||||
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||
@@ -113,11 +114,11 @@ public class LoadingPickService {
|
||||
@Value("${baiyue.security.internal-service-token:test-internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
/** Opens the field task only after planning, transport identity and ocean release have converged. */
|
||||
/** Opens the field task after planning and ocean release; the real container number may be filled later. */
|
||||
@Transactional
|
||||
public void beginIfReady(long containerBookingId, long operatorId, String operatorName) {
|
||||
var booking = smartLoadingRepository.booking(containerBookingId);
|
||||
if (booking == null || !StringUtils.hasText(booking.getContainerNo())) return;
|
||||
if (booking == null) return;
|
||||
var plan = smartLoadingRepository.planByBooking(containerBookingId);
|
||||
if (plan == null || plan.getPreloadPushedAt() == null || !"EDITING".equals(plan.getPlanStatus())) return;
|
||||
var links = loadingModelBridge.byLoadingPlan(plan.getId());
|
||||
@@ -137,10 +138,6 @@ public class LoadingPickService {
|
||||
if (!Set.of("PENDING", "CONFIRMING").contains(link.preloadStatus())) {
|
||||
throw conflict("预装清单已确认或当前状态不允许确认");
|
||||
}
|
||||
var booking = smartLoadingRepository.booking(link.containerBookingId());
|
||||
if (booking == null || !StringUtils.hasText(booking.getContainerNo())) {
|
||||
throw conflict("确认预装前必须录入真实集装箱号");
|
||||
}
|
||||
var allocation = oceanBookingRepository.allocationByContainer(link.containerBookingId());
|
||||
if (allocation == null) throw conflict("确认预装前必须关联真实订舱柜");
|
||||
var oceanBooking = oceanBookingRepository.find(allocation.getOceanBookingId());
|
||||
@@ -462,6 +459,7 @@ public class LoadingPickService {
|
||||
requireSameCompletionCommand(replay, taskId, requestHash, operatorId);
|
||||
if ("FAILED".equals(replay.getCommandStatus())) {
|
||||
LoadingPickTaskEntity failed = requiredTaskForUpdate(taskId);
|
||||
requireCompletionContainerNumber(taskId);
|
||||
if (!"COMPLETION_FAILED".equals(failed.getTaskStatus())
|
||||
|| !completionCommands.resume(replay.getId()) || repository.resumeCompletion(taskId) != 1) {
|
||||
throw conflict("装柜完成命令状态已变化,请刷新后重试");
|
||||
@@ -485,6 +483,7 @@ public class LoadingPickService {
|
||||
if (!request.expectedTaskVersion().equals(task.getVersion())) {
|
||||
throw conflict("PDA任务版本已变化,请刷新后复核");
|
||||
}
|
||||
requireCompletionContainerNumber(taskId);
|
||||
if (!repository.allTaskItemsResolved(taskId)) {
|
||||
throw conflict("存在尚未处理的PDA拣货明细,不能确认装柜完成");
|
||||
}
|
||||
@@ -514,6 +513,18 @@ public class LoadingPickService {
|
||||
return view(requiredTask(taskId), null);
|
||||
}
|
||||
|
||||
private void requireCompletionContainerNumber(long taskId) {
|
||||
ContainerBookingEntity booking = repository.activeBookingByTask(taskId);
|
||||
if (booking == null || !StringUtils.hasText(booking.getContainerNo())) {
|
||||
throw conflict("PDA确认装柜完成前必须录入真实集装箱号");
|
||||
}
|
||||
try {
|
||||
Iso6346ContainerNumber.require(booking.getContainerNo());
|
||||
} catch (BizException exception) {
|
||||
throw conflict("PDA确认装柜完成前必须录入符合ISO 6346的真实集装箱号");
|
||||
}
|
||||
}
|
||||
|
||||
/** Called only by the completion worker, after its durable lease has committed. */
|
||||
public void processCompletion(LoadingCompletionCommandEntity command, String worker) {
|
||||
long taskId = command.getPickTaskId();
|
||||
|
||||
@@ -49,8 +49,8 @@ paths:
|
||||
patch:
|
||||
summary: 修改预装运输信息
|
||||
description: >-
|
||||
唯一纠错入口。仅已推送且仍为 EDITING、真实柜号已由 ocean-booking-link 首次写入、PDA任务尚未领取且无任何物理作业事实时允许;
|
||||
containerNo 为空表示保持当前值,非空时服务端移除格式空白、转大写并校验ISO 6346结构、设备类别和校验位。禁止清空,LOADING/COMPLETED全部拒绝。
|
||||
仅已推送且仍为 EDITING、PDA任务尚未领取且无任何物理作业事实时允许。可首次补录真实集装箱号;已有柜号的纠正必须提供原因和当前版本;
|
||||
containerNo 为 null 表示保持当前值,非空时服务端移除格式空白、转大写并校验ISO 6346结构、设备类别和校验位,空白字符串非法且禁止清空。
|
||||
x-permission: platform:firstleg:preload:edit
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/PlanId'}
|
||||
@@ -66,7 +66,7 @@ paths:
|
||||
/api/platform/v1/first-leg/loading-plans/{planId}/confirm:
|
||||
post:
|
||||
summary: 原子确认整份预装计划
|
||||
description: 以 loadingPlanId 为唯一范围,原子确认全部已分配入库预装票据。要求计划已 push 且仍为 EDITING、真实集装箱号已录入、稳定 bridge 完整、真实订舱已关联并为 RELEASED;任一票不满足均不写入。成功后唯一创建待领取 PDA task,claim 后才推进 LOADING。同一 Idempotency-Key 与相同请求精确重放原响应,不同 Key 重复确认返回业务冲突。
|
||||
description: 以 loadingPlanId 为唯一范围,原子确认全部已分配入库预装票据。要求计划已 push 且仍为 EDITING、稳定 bridge 完整、真实订舱已关联并为 RELEASED;真实集装箱号可在PDA确认装柜完成前补录。任一票不满足均不写入。成功后唯一创建待领取 PDA task,claim 后才推进 LOADING。同一 Idempotency-Key 与相同请求精确重放原响应,不同 Key 重复确认返回业务冲突。
|
||||
x-permission: platform:firstleg:preload:edit
|
||||
parameters: [{$ref: '#/components/parameters/PlanId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody:
|
||||
@@ -313,11 +313,11 @@ components:
|
||||
pdaTaskStatus: {type: string, nullable: true, description: PDA task 当前状态}
|
||||
confirmationStatus: {type: string, enum: [PENDING, PARTIALLY_CONFIRMED, CONFIRMED], description: 整份计划的预装确认状态}
|
||||
canConfirm: {type: boolean, description: 当前是否可调用计划级确认接口}
|
||||
readyForPda: {type: boolean, description: PDA 创建前置条件是否全部满足;task 已创建后仍为 true}
|
||||
readyForPda: {type: boolean, description: PDA 创建前置条件是否全部满足;不依赖真实集装箱号,task 已创建后仍为 true}
|
||||
blockingReasons:
|
||||
type: array
|
||||
description: 阻止计划确认的稳定原因码
|
||||
items: {type: string, enum: [PRELOAD_NOT_PUSHED, LOADING_PLAN_NOT_EDITING, CONTAINER_NO_MISSING, PRELOAD_LINK_MISSING, OCEAN_BOOKING_NOT_LINKED, OCEAN_BOOKING_NOT_RELEASED, PDA_TASK_ALREADY_CREATED, PRELOAD_ALREADY_CONFIRMED]}
|
||||
items: {type: string, enum: [PRELOAD_NOT_PUSHED, LOADING_PLAN_NOT_EDITING, PRELOAD_LINK_MISSING, OCEAN_BOOKING_NOT_LINKED, OCEAN_BOOKING_NOT_RELEASED, PDA_TASK_ALREADY_CREATED, PRELOAD_ALREADY_CONFIRMED]}
|
||||
ConfirmPreloadRequest:
|
||||
type: object
|
||||
required: [expectedVersion, operationReason]
|
||||
@@ -428,4 +428,4 @@ components:
|
||||
containerNo: {type: string, maxLength: 64, nullable: true, description: 新真实集装箱号;null表示保持当前值,空白字符串非法,禁止清空}
|
||||
voyageNo: {type: string, maxLength: 64, nullable: true, description: 航次}
|
||||
expectedVersion: {type: integer, format: int32, minimum: 0, description: 当前乐观锁版本}
|
||||
operationReason: {type: string, minLength: 1, maxLength: 500, description: 必填纠错原因}
|
||||
operationReason: {type: string, minLength: 1, maxLength: 500, nullable: true, description: 纠正已有真实集装箱号时必填;首次补录或保持当前柜号时可省略}
|
||||
|
||||
+2
-2
@@ -100,10 +100,10 @@ class LoadingListControllerContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void transportCorrectionRequiresReasonAndKeepsNullContainerAsUnchanged() {
|
||||
void transportCorrectionReasonIsRuntimeConditionalAndNullContainerKeepsCurrentValue() {
|
||||
assertThat(component(TransportUpdateRequest.class, "operationReason").getAccessor()
|
||||
.getAnnotation(NotBlank.class))
|
||||
.isNotNull();
|
||||
.isNull();
|
||||
assertThat(component(TransportUpdateRequest.class, "containerNo").getAccessor()
|
||||
.getAnnotation(NotBlank.class))
|
||||
.isNull();
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ class LoadingListQueryMapperContractTest {
|
||||
.contains("SET b.container_no = #{containerNo}, b.voyage_no = #{voyageNo}")
|
||||
.contains("p.plan_status='EDITING'")
|
||||
.contains("preload_pushed_at IS NOT NULL")
|
||||
.contains("#{containerNo} IS NOT NULL")
|
||||
.doesNotContain("#{containerNo} IS NOT NULL")
|
||||
.contains("fl_loading_pick_task", "fl_loading_pick_assignment")
|
||||
.doesNotContain("DELETE")
|
||||
.doesNotContain("fl_loading_plan_item");
|
||||
|
||||
@@ -72,4 +72,30 @@ class LoadingListRepositoryTest {
|
||||
assertThat(confirmed.planId()).isEqualTo(41L);
|
||||
assertThat(confirmed.containerBookingId()).isEqualTo(61L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingContainerNumberDoesNotBlockPreloadConfirmationOrPdaReadiness() {
|
||||
AggregateRow row = new AggregateRow();
|
||||
row.setPlanId(41L);
|
||||
row.setContainerBookingId(61L);
|
||||
row.setPlanNo("LP-41");
|
||||
row.setPlanStatus("EDITING");
|
||||
row.setVersion(1);
|
||||
row.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
row.setPreloadLinkCount(2L);
|
||||
row.setConfirmedPreloadCount(0L);
|
||||
row.setOceanBookingId(81L);
|
||||
row.setOceanBookingStatus("RELEASED");
|
||||
when(mapper.selectItems(41L)).thenReturn(List.of());
|
||||
when(mapper.selectHistories(41L)).thenReturn(List.of());
|
||||
|
||||
var pending = repository.preloadDetail(row).summary().readiness();
|
||||
assertThat(pending.containerNoReady()).isFalse();
|
||||
assertThat(pending.canConfirm()).isTrue();
|
||||
assertThat(pending.blockingReasons()).doesNotContain("CONTAINER_NO_MISSING");
|
||||
|
||||
row.setConfirmedPreloadCount(2L);
|
||||
var confirmed = repository.preloadDetail(row).summary().readiness();
|
||||
assertThat(confirmed.readyForPda()).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,20 +208,43 @@ class LoadingListServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadingPlanAndMissingInitialOceanBookingNumberBothRejectCorrection() {
|
||||
void loadingPlanRejectsTransportUpdateButPushedEditingPlanAllowsFirstContainerFill() {
|
||||
AggregateRow loading = row("LOADING", 3);
|
||||
loading.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
loading.setContainerNo("MSCU6639870");
|
||||
AggregateRow missing = row("EDITING", 3);
|
||||
missing.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
when(repository.aggregate(10L)).thenReturn(loading, missing);
|
||||
AggregateRow afterFill = row("EDITING", 4);
|
||||
afterFill.setPreloadPushedAt(missing.getPreloadPushedAt());
|
||||
afterFill.setContainerNo("CSQU3054383");
|
||||
when(repository.aggregate(10L)).thenReturn(loading, missing, afterFill);
|
||||
when(repository.updateTransport(10L, 3, "CSQU3054383", null, 9L, "operator"))
|
||||
.thenReturn(1);
|
||||
when(repository.preloadDetail(afterFill)).thenReturn(
|
||||
new com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail(null, List.of(), List.of()));
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "loading-key",
|
||||
new TransportUpdateRequest("CSQU3054383", null, 3, "作业后纠错")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("PDA尚未开始");
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "missing-key",
|
||||
new TransportUpdateRequest("CSQU3054383", null, 3, "首次补录")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("首次写入");
|
||||
service.updateTransport(10L, 100L, 9L, "operator", "missing-key",
|
||||
new TransportUpdateRequest("CSQU3054383", null, 3, null));
|
||||
|
||||
verify(repository).updateTransport(10L, 3, "CSQU3054383", null, 9L, "operator");
|
||||
verify(loadingPickService).beginIfReady(30L, 9L, "operator");
|
||||
}
|
||||
|
||||
@Test
|
||||
void correctingExistingContainerNumberStillRequiresReason() {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
before.setContainerNo("MSCU6639870");
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "reason-key",
|
||||
new TransportUpdateRequest("CSQU3054383", null, 3, null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("纠错原因");
|
||||
|
||||
verify(repository, never()).updateTransport(anyLong(), anyInt(), any(), any(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -113,7 +113,7 @@ class LoadingPickServiceTest {
|
||||
}).when(completionTransaction).executeWithoutResult(any());
|
||||
ContainerBookingEntity booking = new ContainerBookingEntity();
|
||||
booking.setId(99L);
|
||||
booking.setContainerNo("CSLU1234567");
|
||||
booking.setContainerNo("MSCU6639870");
|
||||
booking.setWarehouseId(101L);
|
||||
booking.setSourceType("SIMULATION");
|
||||
booking.setRecordStatus("ACTIVE");
|
||||
@@ -124,9 +124,9 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void opensTaskOnlyAfterReleasedBookingAndRealContainerNumber() {
|
||||
void opensTaskAfterReleasedBookingWithoutWaitingForRealContainerNumber() {
|
||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
booking.setId(40L); booking.setContainerNo("MSCU-001");
|
||||
booking.setId(40L);
|
||||
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
|
||||
plan.setId(10L); plan.setContainerBookingId(40L); plan.setPlanStatus("EDITING");
|
||||
plan.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
@@ -136,7 +136,7 @@ class LoadingPickServiceTest {
|
||||
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)));
|
||||
new Link(1L, 1L, 50L, 2L, "CONFIRMED", 1, 10L, 40L, null, "EDITING", 1, 1, null)));
|
||||
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
|
||||
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
|
||||
when(repository.taskByPlan(10L)).thenReturn(null);
|
||||
@@ -191,11 +191,11 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void planConfirmationGateRequiresRealContainerAndReleasedBooking() {
|
||||
void planConfirmationGateRequiresReleasedBookingButNotRealContainerNumber() {
|
||||
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");
|
||||
booking.setId(40L);
|
||||
var allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setOceanBookingId(90L);
|
||||
var ocean = new OceanBookingEntity();
|
||||
@@ -209,17 +209,21 @@ class LoadingPickServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void planConfirmationGateRejectsMissingRealContainerNumber() {
|
||||
void planConfirmationGateAllowsMissingRealContainerNumber() {
|
||||
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);
|
||||
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);
|
||||
|
||||
assertThatThrownBy(() -> service.requirePreloadConfirmationReady(10L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("真实集装箱号");
|
||||
verify(oceanBookingRepository, never()).allocationByContainer(anyLong());
|
||||
assertThat(service.requirePreloadConfirmationReady(10L)).containsExactly(link);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -775,6 +779,25 @@ class LoadingPickServiceTest {
|
||||
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionRejectsMissingRealContainerNumberBeforeCreatingCommand() {
|
||||
LoadingPickTaskEntity loading = task();
|
||||
ContainerBookingEntity booking = new ContainerBookingEntity();
|
||||
booking.setId(99L);
|
||||
booking.setWarehouseId(101L);
|
||||
booking.setRecordStatus("ACTIVE");
|
||||
when(repository.taskForUpdate(20L)).thenReturn(loading);
|
||||
when(repository.activeBookingByTask(20L)).thenReturn(booking);
|
||||
|
||||
assertThatThrownBy(() -> service.complete(20L,
|
||||
new CompleteLoadingRequest(0, "complete-missing-container", "checked"), 9L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("确认装柜完成前");
|
||||
|
||||
verify(repository, never()).allTaskItemsResolved(20L);
|
||||
verify(completionCommands, never()).insert(any());
|
||||
verify(repository, never()).acceptCompletion(anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentOperationIdIsRejectedImmediatelyWithConflict() {
|
||||
// different operationId is a canonical A0409 CONFLICT while the durable command is active.
|
||||
|
||||
Reference in New Issue
Block a user