fix(order): complete root fulfillment only after effective remainders

This commit is contained in:
2026-09-30 13:49:40 +08:00
parent bd73c7a326
commit 13028d8a5a
25 changed files with 684 additions and 39 deletions
@@ -39,9 +39,21 @@ class InternalInboundOrderReferenceControllerTest {
private OrderDeclarationSyncService declarationSync;
@MockBean
private OrderRemarksSyncService remarksSync;
@MockBean private com.baiyue.firstleg.service.OrderFulfillmentSnapshotService fulfillmentSnapshot;
@Resource
private InternalInboundOrderController controller;
@Test
void completionSnapshotRequiresInternalTokenAndDelegatesExactCommand() {
var command = new com.baiyue.firstleg.dto.OrderFulfillmentCompleted(11, 21,
"FL-21", "IN-21", LocalDateTime.of(2026, 9, 30, 12, 0));
assertThatThrownBy(() -> controller.completeFulfillmentSnapshot("wrong", command)).isInstanceOf(BizException.class);
org.mockito.Mockito.verifyNoInteractions(fulfillmentSnapshot);
when(fulfillmentSnapshot.complete(command)).thenReturn(0);
assertThat(controller.completeFulfillmentSnapshot("internal-token", command).data()).isZero();
verify(fulfillmentSnapshot).complete(command);
}
@Test
void shouldHandleCase01() {
var request = new InboundOrderReference.BatchRequest(10001L, List.of(77L, 77L, 78L));
@@ -0,0 +1,52 @@
package com.baiyue.firstleg.service;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.OrderFulfillmentCompleted;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.mapper.InboundOrderMapper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;
class OrderFulfillmentSnapshotServiceTest {
@BeforeAll static void metadata() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "snapshot-test"), InboundOrderEntity.class);
}
@Test void completedAndReplaySyncOnlyAuthenticatedSource() {
var mapper = mock(InboundOrderMapper.class);
when(mapper.selectList(any())).thenReturn(List.of(row("FL21", "IN21", "DELIVERED")));
when(mapper.markOrderSnapshotCompleted(eq(11L),eq(21L),any())).thenReturn(1,0);
var service = new OrderFulfillmentSnapshotService(mapper);
assertThat(service.complete(command())).isEqualTo(1);
assertThat(service.complete(command())).isZero();
var query = org.mockito.ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);
verify(mapper, times(2)).selectList(query.capture());
assertThat(query.getValue().getSqlSegment()).contains("tenant_id", "order_id", "deleted", "FOR UPDATE");
}
@Test void unDeliveredFragmentBlocksAllSnapshotWrites() {
var mapper = mock(InboundOrderMapper.class);
when(mapper.selectList(any())).thenReturn(List.of(row("FL21", "IN21", "DELIVERED"), row("FL21", "IN21", "LOADED")));
assertThatThrownBy(() -> new OrderFulfillmentSnapshotService(mapper).complete(command())).isInstanceOf(BizException.class);
verify(mapper,never()).markOrderSnapshotCompleted(anyLong(),anyLong(),any());
}
@Test void missingOrForeignSnapshotBlocksAllWrites() {
var mapper = mock(InboundOrderMapper.class);
when(mapper.selectList(any())).thenReturn(List.of(), List.of(row("OTHER", "IN21", "DELIVERED")));
var service = new OrderFulfillmentSnapshotService(mapper);
assertThatThrownBy(() -> service.complete(command())).isInstanceOf(BizException.class);
assertThatThrownBy(() -> service.complete(command())).isInstanceOf(BizException.class);
verify(mapper,never()).markOrderSnapshotCompleted(anyLong(),anyLong(),any());
}
private OrderFulfillmentCompleted command() { return new OrderFulfillmentCompleted(11,21,"FL21","IN21", LocalDateTime.of(2026,9,30,12,0)); }
private InboundOrderEntity row(String orderNo, String inboundCode, String process) {
var row = new InboundOrderEntity(); row.setId(1L);row.setTenantId(11L);row.setOrderId(21L);
row.setOrderNo(orderNo);row.setInboundCode(inboundCode);row.setProcessStatus(process); return row;
}
}
@@ -36,7 +36,7 @@ class OverseasDeliveryCompletedConsumerTest {
when(inbounds.completeOverseasDelivery(eq(88L), eq(101L), any())).thenReturn(1);
when(inbounds.countNotDeliveredByOrder(88L, 201L)).thenReturn(0);
when(orders.complete(eq("token"), any())).thenReturn(ApiResponse.success(
new OrderFulfillmentCompletionClient.Result(201L, "COMPLETED", false)));
new OrderFulfillmentCompletionClient.Result(201L, "COMPLETED", false, 201L, "COMPLETED")));
consumer.consume(event(json));
@@ -72,10 +72,48 @@ class OverseasDeliveryCompletedConsumerTest {
verify(inbounds, never()).markOrderSnapshotCompleted(anyLong(), anyLong(), any());
}
private EventEnvelope event(ObjectMapper json) {
@Test
void acceptsRootShipmentFactWithoutPrematureCompletedSnapshot() {
verifyCompletionResponse(new OrderFulfillmentCompletionClient.Result(201L, "PROCESSING", false,
201L, "PROCESSING"), false, false);
}
@Test
void marksOnlyChildWhenRootStillHasOutstandingBranch() {
verifyCompletionResponse(new OrderFulfillmentCompletionClient.Result(201L, "COMPLETED", false,
200L, "PROCESSING"), true, false);
}
@Test
void marksChildAndRootOnLastSignedBranch() {
verifyCompletionResponse(new OrderFulfillmentCompletionClient.Result(201L, "COMPLETED", false,
200L, "COMPLETED"), true, true);
}
private void verifyCompletionResponse(OrderFulfillmentCompletionClient.Result result,
boolean sourceCompleted, boolean rootCompleted) {
InboundOrderMapper inbounds = mock(InboundOrderMapper.class);
OrderFulfillmentCompletionClient orders = mock(OrderFulfillmentCompletionClient.class);
ObjectMapper json = new ObjectMapper().registerModule(new JavaTimeModule());
OverseasDeliveryCompletedConsumer consumer = new OverseasDeliveryCompletedConsumer(inbounds,
mock(InboundStatusHistoryMapper.class), mock(ContainerEventInboxMapper.class), orders, json);
ReflectionTestUtils.setField(consumer, "internalToken", "token");
InboundOrderEntity inbound = new InboundOrderEntity();
inbound.setId(101L); inbound.setTenantId(88L); inbound.setOrderId(201L);
inbound.setOrderNo("FL-201"); inbound.setInboundCode("IN-1"); inbound.setDestinationCode("ONT8");
inbound.setProcessStatus("DELIVERED");
when(inbounds.selectActiveByIdForUpdate(101L)).thenReturn(inbound);
when(orders.complete(eq("token"), any())).thenReturn(ApiResponse.success(result));
consumer.consume(event(json, result.rootOrderId().toString()));
verify(inbounds, times(sourceCompleted ? 1 : 0)).markOrderSnapshotCompleted(eq(88L), eq(201L), any());
verify(inbounds, times(rootCompleted ? 1 : 0)).markOrderSnapshotCompleted(eq(88L), eq(200L), any());
}
private EventEnvelope event(ObjectMapper json) { return event(json, "201"); }
private EventEnvelope event(ObjectMapper json, String rootOrderId) {
Instant time = Instant.parse("2026-09-15T08:00:00Z");
var payload = new OverseasDeliveryCompletedV1("1", "DL-ONT8", "11", "LM-11",
"101", "IN-1", "201", "FL-201", "201", "ONT8", time);
"101", "IN-1", "201", "FL-201", rootOrderId, "ONT8", time);
var node = json.valueToTree(payload);
return new EventEnvelope("00000000-0000-4000-8000-000000000001",
EventTypes.OVERSEAS_DELIVERY_COMPLETED, 1, time,