fix(firstleg): enforce strict manual node progression

This commit is contained in:
2026-08-26 15:44:05 +08:00
parent 2ebea9e789
commit 57a2f6ef28
5 changed files with 22 additions and 60 deletions
@@ -24,9 +24,8 @@ class CarrierScheduleQueryOpenApiContractTest {
.contains("enum: [READY, MANUAL_REQUIRED, STOPPED]")
.contains("latestActualNode:")
.contains("latestActualAt:")
.contains("current latestActualNode may append an audited correction fact")
.contains("replace latestActualAt")
.contains("cannot be earlier")
.contains("manual actual node is equal to or earlier than latestActualNode")
.contains("strictly greater sequence than latestActualNode")
.contains("internal execution fields are intentionally absent")
.doesNotContain("enum: [READY, MANUAL_REQUIRED, ARRIVED")
.doesNotContain("/{queryId}/attempts:")
@@ -293,43 +293,25 @@ class CarrierScheduleQueryServiceTest {
}
@Test
void manualActualMayCorrectCurrentNodeTimeThroughNewImmutableFact() {
CarrierScheduleQueryEntity before = task(0, 3);
before.setStatus("READY"); before.setLatestStandardNode("PICKED_UP");
before.setLatestNodeObservedAt(NOW.minusDays(1));
CarrierScheduleQueryEntity after = task(0, 4);
after.setStatus("READY"); after.setLatestStandardNode("PICKED_UP");
after.setLatestNodeObservedAt(NOW.minusDays(2));
when(repository.lock(1L)).thenReturn(before); when(repository.find(1L)).thenReturn(after);
when(repository.insertIfAbsent(any(CarrierScheduleNodeFactEntity.class))).thenReturn(true);
when(repository.updateState(any(), any(Integer.class), anyLong(), anyString())).thenReturn(1);
var result = service.addManualNode(1L, 7L, "admin", "manual-picked-up-correction",
new ManualNodeRequest(3, "PICKED_UP", NOW, null, NOW.minusDays(2), null,
"Los Angeles", "修正提柜时间"));
assertThat(result.query().query().latestActualNode()).isEqualTo("PICKED_UP");
assertThat(result.query().query().latestActualAt()).isEqualTo(NOW.minusDays(2));
ArgumentCaptor<CarrierScheduleNodeFactEntity> fact =
ArgumentCaptor.forClass(CarrierScheduleNodeFactEntity.class);
verify(repository).insertIfAbsent(fact.capture());
assertThat(fact.getValue().getActualAt()).isEqualTo(NOW.minusDays(2));
verify(repository).updateState(before, 3, 7L, "admin");
verify(repository).completeOperation(eq(1L), eq(7L), eq("manual-picked-up-correction"), anyString());
}
@Test
void manualActualRejectsEarlierStandardNodeWithoutAppendingFact() {
void manualActualRejectsSameAndEarlierNodesIncludingSameKeyRetryWithoutDrift() {
CarrierScheduleQueryEntity task = task(0, 3);
task.setStatus("READY"); task.setLatestStandardNode("PICKED_UP");
task.setLatestNodeObservedAt(NOW.minusDays(1));
when(repository.lock(1L)).thenReturn(task);
ManualNodeRequest sameNode = new ManualNodeRequest(3, "PICKED_UP", NOW, null,
NOW.plusDays(1), null, "Los Angeles", "重复确认提柜");
assertThatThrownBy(() -> service.addManualNode(1L, 7L, "admin", "manual-picked-up-again", sameNode))
.isInstanceOf(BizException.class)
.hasMessage("人工实际节点必须严格晚于当前最新实际节点");
assertThatThrownBy(() -> service.addManualNode(1L, 7L, "admin", "manual-picked-up-again", sameNode))
.isInstanceOf(BizException.class)
.hasMessage("人工实际节点必须严格晚于当前最新实际节点");
assertThatThrownBy(() -> service.addManualNode(1L, 7L, "admin", "manual-pickup-ready-late",
new ManualNodeRequest(3, "PICKUP_READY", NOW, null, NOW.plusDays(2), null,
"Los Angeles", "倒序补录可提柜")))
.isInstanceOf(BizException.class)
.hasMessageContaining("不得早于当前最新实际节点");
.hasMessage("人工实际节点必须严格晚于当前最新实际节点");
verify(repository, never()).insertIfAbsent(any(CarrierScheduleNodeFactEntity.class));
verify(repository, never()).updateState(any(), any(Integer.class), anyLong(), anyString());