This commit is contained in:
2026-09-02 10:41:32 +08:00
parent 31996491be
commit 8e7835fe86
10 changed files with 529 additions and 0 deletions
@@ -6,6 +6,7 @@ import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
import com.baiyue.firstleg.service.InboundOrderCreationService;
import com.baiyue.firstleg.service.InboundCartonLabelService;
import com.baiyue.firstleg.service.UnifiedInboundQueryService;
import com.baiyue.firstleg.service.OrderDeclarationSyncService;
import com.baiyue.firstleg.dto.UnifiedInboundQuery;
import com.baiyue.common.core.api.PageResult;
import jakarta.annotation.Resource;
@@ -31,6 +32,8 @@ class InternalInboundOrderReferenceControllerTest {
private InboundCartonLabelService cartonLabels;
@MockBean
private UnifiedInboundQueryService unifiedQueryService;
@MockBean
private OrderDeclarationSyncService declarationSync;
@Resource
private InternalInboundOrderController controller;
@@ -119,6 +119,9 @@ class PriceBookContractTest {
String firstlegOpenapi = new ClassPathResource("openapi/firstleg-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(firstlegOpenapi)
.contains("/internal/firstleg/v1/inbounds/order-channel-changes/validate:")
.contains("/internal/firstleg/v1/inbounds/order-declaration-updated:")
.contains("同版本异事件返回 A0409")
.contains("/internal/firstleg/v1/pricing/channels/validate:")
.contains("/internal/firstleg/v1/pricing/order-channel-changes/validate:")
.contains("/internal/firstleg/v1/pricing/order-channel-changes/apply:")
@@ -0,0 +1,23 @@
package com.baiyue.firstleg.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class OrderDeclarationSyncMapperContractTest {
@Test
void mapperIsExplicitAndProjectionUpdateCannotOverwriteOperationalFacts() throws Exception {
assertThat(OrderDeclarationSyncMapper.class).hasAnnotation(Mapper.class);
String sql = String.join(" ", OrderDeclarationSyncMapper.class
.getDeclaredMethod("updateAllowedSnapshot", long.class, long.class, long.class, String.class,
String.class, String.class, java.time.LocalDateTime.class, String.class, String.class, long.class)
.getAnnotation(Update.class).value());
assertThat(sql)
.contains("public_remark", "expected_arrival_at", "channel_code", "channel_name")
.doesNotContain("actual_net_weight", "actual_volume", "fee_status", "SET inbound_code",
"pallet", "box_code", "process_status");
}
}
@@ -0,0 +1,129 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.OrderDeclarationSync;
import com.baiyue.firstleg.mapper.OrderDeclarationSyncMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import java.math.BigDecimal;
import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.util.HexFormat;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig({OrderDeclarationSyncService.class, OrderDeclarationSyncServiceTest.JsonConfig.class})
class OrderDeclarationSyncServiceTest {
@Configuration
static class JsonConfig {
@Bean ObjectMapper objectMapper() { return new ObjectMapper().findAndRegisterModules(); }
}
@MockBean private OrderDeclarationSyncMapper mapper;
@Resource private OrderDeclarationSyncService service;
@Resource private ObjectMapper json;
@Test
void allowsOnlyCompatibleUnallocatedFulfillment() {
when(mapper.gate(10001L, 77L, "MATSON_NEW")).thenReturn(
row("WAIT_SPLIT", 0, 0, 0, 0));
var response = service.validateChannelChange(
new OrderDeclarationSync.ChannelChangeValidationRequest(10001L, 77L, 4L, "matson_new"));
assertThat(response.channelCode()).isEqualTo("MATSON_NEW");
assertThat(response.mainStatus()).isEqualTo("WAIT_SPLIT");
}
@Test
void rejectsPdaFactWithoutMutatingAnything() {
when(mapper.gate(10001L, 77L, "MATSON_NEW")).thenReturn(
row("WAIT_SPLIT", 0, 0, 1, 1));
assertThatThrownBy(() -> service.validateChannelChange(
new OrderDeclarationSync.ChannelChangeValidationRequest(10001L, 77L, 4L, "MATSON_NEW")))
.isInstanceOf(BizException.class)
.hasMessageContaining("PDA装柜任务");
verify(mapper, never()).updateAllowedSnapshot(anyLong(), anyLong(), anyLong(), anyString(), anyString(),
any(), any(), anyString(), anyString(), anyLong());
}
@Test
void persistsVersionedDeclarationAndUpdatesOnlyAllowedInboundProjection() {
var event = event(4, "event-4");
when(mapper.lockInbound(10001L, 77L)).thenReturn(row("WAIT_MEASURE", null, null, null, null));
when(mapper.insertSnapshot(anyLong(), anyLong(), anyString(), anyInt(), anyString(), any(), any(),
anyString(), anyString(), anyString(), anyString(), any())).thenReturn(1);
when(mapper.updateAllowedSnapshot(10001L, 88L, 77L, "FL-77", "RK-77", "客户新备注",
event.expectedArrivalAt(), "MATSON_NEW", "新渠道", 9L)).thenReturn(1);
service.consume(event);
verify(mapper).updateAllowedSnapshot(10001L, 88L, 77L, "FL-77", "RK-77", "客户新备注",
event.expectedArrivalAt(), "MATSON_NEW", "新渠道", 9L);
}
@Test
void ignoresOlderVersionWithoutOverwritingCurrentProjection() {
var event = event(4, "event-4");
when(mapper.lockInbound(10001L, 77L)).thenReturn(row("WAIT_MEASURE", null, null, null, null));
when(mapper.lockSnapshot(10001L, 77L)).thenReturn(
new OrderDeclarationSyncMapper.SnapshotRow(10001L, 77L, "event-5", 5, "hash"));
service.consume(event);
verify(mapper, never()).insertSnapshot(anyLong(), anyLong(), anyString(), anyInt(), anyString(), any(),
any(), anyString(), anyString(), anyString(), anyString(), any());
verify(mapper, never()).updateAllowedSnapshot(anyLong(), anyLong(), anyLong(), anyString(), anyString(),
any(), any(), anyString(), anyString(), anyLong());
}
@Test
void sameEventAndPayloadIsIdempotent() throws Exception {
var event = event(4, "event-4");
String hash = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
.digest(json.writeValueAsBytes(event)));
when(mapper.lockInbound(10001L, 77L)).thenReturn(row("WAIT_MEASURE", null, null, null, null));
when(mapper.byEvent(10001L, "event-4")).thenReturn(
new OrderDeclarationSyncMapper.SnapshotRow(10001L, 77L, "event-4", 4, hash));
service.consume(event);
verify(mapper, never()).lockSnapshot(anyLong(), anyLong());
verify(mapper, never()).updateAllowedSnapshot(anyLong(), anyLong(), anyLong(), anyString(), anyString(),
any(), any(), anyString(), anyString(), anyLong());
}
private static OrderDeclarationSyncMapper.GateRow row(String status, Integer group, Integer split,
Integer preload, Integer pda) {
return new OrderDeclarationSyncMapper.GateRow(88L, 77L, "FL-77", "RK-77", status,
"MATSON_OLD", group, split, preload, pda);
}
private static OrderDeclarationSync.UpdatedEvent event(int version, String eventId) {
var cargo = new OrderDeclarationSync.Cargo(1, "衣服", "clothes", "SKU-1", null, 2, 20,
new BigDecimal("10"), new BigDecimal("20"), new BigDecimal("30"), new BigDecimal("37"),
new BigDecimal("0.102"), null, null, null, null, null, null, "CNY",
null, null, null, null);
return new OrderDeclarationSync.UpdatedEvent(1, eventId, 10001L, 77L, "FL-77", "RK-77",
version, 9L, LocalDateTime.of(2026, 9, 2, 10, 0), "客户新备注",
LocalDateTime.of(2026, 9, 3, 10, 0), "MATSON_NEW", "新渠道", true,
List.of(cargo), List.of(new OrderDeclarationSync.FieldChange(null, "channelCode",
"MATSON_OLD", "MATSON_NEW")));
}
}