fix: validate overseas handover before settlement
This commit is contained in:
@@ -43,9 +43,7 @@ public interface LoadingCompletedEventMapper {
|
||||
AND actual_task.loading_plan_id = p.id
|
||||
JOIN fl_inbound_order o ON o.id = i.inbound_id AND o.tenant_id = i.account_id AND o.deleted = 0
|
||||
LEFT JOIN fl_inbound_order root_order ON root_order.id = COALESCE(o.root_inbound_id, o.id)
|
||||
WHERE p.id = #{planId} AND p.plan_status = 'COMPLETED'
|
||||
AND o.process_status = 'LOADED'
|
||||
AND i.package_count > 0
|
||||
WHERE p.id = #{planId} AND i.package_count > 0
|
||||
ORDER BY i.account_id, i.inbound_id, i.inbound_detail_id, i.id
|
||||
""")
|
||||
List<Row> rows(@Param("planId") long planId);
|
||||
|
||||
+13
-3
@@ -48,16 +48,26 @@ public class LoadingCompletedOutboxService {
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
public void publish(long planId) {
|
||||
preparedEnvelopes(planId).forEach(this::insert);
|
||||
}
|
||||
|
||||
/** 在任何入库结算前验证跨区交接快照,避免整托/箱唛冲突留下半完成状态。 */
|
||||
public void requirePublishable(long planId) {
|
||||
preparedEnvelopes(planId);
|
||||
}
|
||||
|
||||
private List<EventEnvelope> preparedEnvelopes(long planId) {
|
||||
List<Row> rows = snapshotMapper.rows(planId);
|
||||
if (rows.isEmpty()) throw new BizException(ResultCode.CONFLICT, "装柜计划没有最终实装入仓单,不能生成尾程交接事件");
|
||||
validateContainerIdentity(rows);
|
||||
validateCustomerSnapshots(rows);
|
||||
Map<Long, List<String>> cartonMarks = cartonLabelPools(rows);
|
||||
List<PalletRow> palletRows = palletRows(planId);
|
||||
rows.stream().collect(java.util.stream.Collectors.groupingBy(Row::getAccountId,
|
||||
return rows.stream().collect(java.util.stream.Collectors.groupingBy(Row::getAccountId,
|
||||
java.util.TreeMap::new, java.util.stream.Collectors.toList()))
|
||||
.forEach((accountId, accountRows) -> insert(envelope(accountId, accountRows,
|
||||
palletRows.stream().filter(item -> accountId.equals(item.getAccountId())).toList(), cartonMarks)));
|
||||
.entrySet().stream().map(entry -> envelope(entry.getKey(), entry.getValue(),
|
||||
palletRows.stream().filter(item -> entry.getKey().equals(item.getAccountId())).toList(), cartonMarks))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** 已开展逐箱打托的入仓单使用现场事实;未开展的存量入仓单继续兼容原托盘分配快照。 */
|
||||
|
||||
@@ -571,6 +571,12 @@ public class LoadingListService {
|
||||
loadingCompletedOutboxService.publish(planId);
|
||||
}
|
||||
|
||||
/** 完成 worker 在远程结算前验证最终跨区快照能够完整生成。 */
|
||||
@Transactional(readOnly = true)
|
||||
public void requireCompletedEventReady(long planId) {
|
||||
loadingCompletedOutboxService.requirePublishable(planId);
|
||||
}
|
||||
|
||||
private AggregateRow requirePlan(long planId) {
|
||||
AggregateRow row = repository.aggregate(planId);
|
||||
if (row == null) {
|
||||
|
||||
@@ -535,6 +535,8 @@ public class LoadingPickService {
|
||||
List<GroupSnapshot> taskGroups = coloadGroups(owners);
|
||||
runCompletionStage("CABINET_EVIDENCE", () ->
|
||||
cabinetEvidenceService.requireComplete(task.getContainerBookingId()));
|
||||
runCompletionStage("COMPLETED_EVENT_READY", () ->
|
||||
loadingListService.requireCompletedEventReady(task.getLoadingPlanId()));
|
||||
|
||||
// Phase 1 is read-only: cabinet evidence is checked once, then every owner must pass its business gates
|
||||
// before any warehouse resource or inbound status can be changed. This prevents a later customer gate from
|
||||
|
||||
+3
-3
@@ -234,7 +234,7 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThat(firstMarks).doesNotContainAnyElementsOf(secondMarks);
|
||||
}
|
||||
|
||||
@Test void snapshotOnlyPublishesFinalizedLoadedInbounds() throws Exception {
|
||||
@Test void snapshotSupportsReadOnlyPreflightBeforeFinalStatusMutation() throws Exception {
|
||||
String rowsSql = String.join(" ", LoadingCompletedEventMapper.class.getMethod("rows", long.class)
|
||||
.getAnnotation(Select.class).value()).replaceAll("\\s+", " ");
|
||||
String palletsSql = String.join(" ", LoadingCompletedEventMapper.class.getMethod("palletRows", long.class)
|
||||
@@ -243,8 +243,8 @@ class LoadingCompletedOutboxServiceTest {
|
||||
.getMethod("actualPalletRows", long.class).getAnnotation(Select.class).value())
|
||||
.replaceAll("\\s+", " ");
|
||||
|
||||
assertThat(rowsSql).contains("p.plan_status = 'COMPLETED'")
|
||||
.contains("o.process_status = 'LOADED'")
|
||||
assertThat(rowsSql).doesNotContain("p.plan_status = 'COMPLETED'")
|
||||
.doesNotContain("o.process_status = 'LOADED'")
|
||||
.contains("i.customer_name")
|
||||
.doesNotContain("b.customer_id AS booking_customer_id")
|
||||
.doesNotContain("b.customer_name")
|
||||
|
||||
@@ -966,6 +966,33 @@ class LoadingPickServiceTest {
|
||||
verify(loadingListService, never()).publishCompletedEvent(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unpublishableCrossRegionSnapshotFailsBeforeAnyOwnerSettlement() {
|
||||
LoadingPickTaskEntity completing = task();
|
||||
completing.setTaskStatus("COMPLETING");
|
||||
when(repository.task(20L)).thenReturn(completing);
|
||||
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||
new LoadingPickItemMapper.InboundOwner(101L, 50L),
|
||||
new LoadingPickItemMapper.InboundOwner(101L, 60L)));
|
||||
doThrow(new BizException(com.baiyue.common.core.api.ResultCode.CONFLICT,
|
||||
"一件代发现场托盘箱唛数与实装箱数不一致"))
|
||||
.when(loadingListService).requireCompletedEventReady(10L);
|
||||
|
||||
assertThatThrownBy(() -> service.processCompletion(
|
||||
completionCommand(905L, "async-unpublishable"), "worker-unpublishable"))
|
||||
.isInstanceOf(LoadingPickService.CompletionProcessingException.class)
|
||||
.satisfies(error -> assertThat(((LoadingPickService.CompletionProcessingException) error).stage())
|
||||
.isEqualTo("COMPLETED_EVENT_READY"));
|
||||
|
||||
verify(settlementService, never()).settleOwner(anyLong(), anyLong(), anyLong(), anyString());
|
||||
verify(loadingDeliveryService, never()).completeAfterPdaSettlement(anyLong(), anyLong(), anyLong(),
|
||||
anyString(), anyString(), anyString());
|
||||
verify(completionCommands, never()).checkpoint(anyLong(), anyLong(), anyLong(), anyInt(), anyString());
|
||||
verify(repository, never()).insert(any(LoadingPickOperationEntity.class));
|
||||
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
|
||||
verify(loadingListService, never()).publishCompletedEvent(anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void workerRestartResumesFromOwnerCheckpointsWithoutRepeatingSettlements() {
|
||||
assertNoTruckCoupling();
|
||||
|
||||
Reference in New Issue
Block a user