Merge remote-tracking branch 'origin/test' into dev
# Conflicts: # order/AGENTS.md # scripts/agents-index-contract.ps1
This commit is contained in:
+25
@@ -6,7 +6,9 @@ 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.firstleg.dto.OrderDeclarationSync;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -15,6 +17,7 @@ import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.util.List;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -31,6 +34,8 @@ class InternalInboundOrderReferenceControllerTest {
|
||||
private InboundCartonLabelService cartonLabels;
|
||||
@MockBean
|
||||
private UnifiedInboundQueryService unifiedQueryService;
|
||||
@MockBean
|
||||
private OrderDeclarationSyncService declarationSync;
|
||||
@Resource
|
||||
private InternalInboundOrderController controller;
|
||||
|
||||
@@ -85,6 +90,26 @@ class InternalInboundOrderReferenceControllerTest {
|
||||
verify(cartonLabels).getOrCreate(10001L, 88L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesExactOrderChannelValidationContract() {
|
||||
var request = new OrderDeclarationSync.ChannelChangeValidationRequest(10001L, 77L, 4L, "MATSON_NEW");
|
||||
var result = new OrderDeclarationSync.ChannelChangeValidationResponse(77L, 4L, "MATSON_NEW", "WAIT_SPLIT");
|
||||
when(declarationSync.validateChannelChange(request)).thenReturn(result);
|
||||
|
||||
assertThat(controller.validateOrderChannelChange("internal-token", request).data()).isEqualTo(result);
|
||||
verify(declarationSync).validateChannelChange(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesExactDeclarationEventConsumerContract() {
|
||||
var event = new OrderDeclarationSync.UpdatedEvent(1, "event-4", 10001L, 77L, "FL-77", "RK-77",
|
||||
4, 9L, LocalDateTime.of(2026, 9, 2, 10, 0), null, null, "MATSON_NEW", "新渠道",
|
||||
true, List.of(), List.of());
|
||||
|
||||
assertThat(controller.updateOrderDeclaration("internal-token", event).successful()).isTrue();
|
||||
verify(declarationSync).consume(event);
|
||||
}
|
||||
|
||||
private static OrderApprovedInboundCommand approvedCommand(String inboundCode, Long carrierId) {
|
||||
return new OrderApprovedInboundCommand(
|
||||
10001L, 77L, "FL-77", inboundCode, 7L, "测试客户", 9L, "义乌仓",
|
||||
|
||||
@@ -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:")
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class OrderDeclarationSyncMapperContractTest {
|
||||
@Test
|
||||
void mapperIsExplicitAndProjectionUpdateCannotOverwriteOperationalFacts() throws Exception {
|
||||
assertThat(OrderDeclarationSyncMapper.class).hasAnnotation(Mapper.class);
|
||||
assertThat(OrderDeclarationSyncMapper.class
|
||||
.getDeclaredMethod("lockInbound", long.class, long.class).getReturnType())
|
||||
.isEqualTo(OrderDeclarationSyncMapper.InboundRow.class);
|
||||
assertThat(OrderDeclarationSyncMapper.InboundRow.class.getRecordComponents())
|
||||
.extracting(java.lang.reflect.RecordComponent::getName)
|
||||
.containsExactly("inboundId", "orderId", "orderNo", "inboundCode", "processStatus", "channelCode");
|
||||
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");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sqlBaselineKeepsCurrentSnapshotAndPermanentEventLedgerSeparate() throws Exception {
|
||||
String sql = new ClassPathResource("db/phase2-inbound-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_order_declaration_snapshot")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_order_declaration_event")
|
||||
.contains("PRIMARY KEY (tenant_id, event_id)")
|
||||
.contains("不覆盖实测事实");
|
||||
}
|
||||
}
|
||||
+16
@@ -96,6 +96,22 @@ class OrderChannelChangePricingServiceTest {
|
||||
verify(repository, never()).applyOrderChannelChange(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameOrderVersionRejectsDifferentChannelNamePayload() {
|
||||
InboundPricingEntity inbound = inbound("UNPRICED");
|
||||
InboundPricingIdempotencyEntity token = token(4L, "COMPLETED");
|
||||
token.setRequestHash(requestHash(request(4)));
|
||||
when(repository.findInboundByOrderForUpdate(10001L, 77L)).thenReturn(inbound);
|
||||
when(repository.findIdempotency(10001L, 0L, "ORDER_CHANNEL_CHANGE", "77:4")).thenReturn(token);
|
||||
Request changedName = new Request(10001L, 77L, 4L, 9L, "MATSON_NEW", "伪造名称",
|
||||
"FBA", 501L, "ONT8", "TAX_INCLUDED");
|
||||
|
||||
assertThatThrownBy(() -> service.apply(changedName))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("同一订单版本已用于不同渠道变更");
|
||||
verify(estimator, never()).estimate(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsOlderOrderVersionAfterNewerVersionWasApplied() {
|
||||
InboundPricingEntity inbound = inbound("UNPRICED");
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
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(inboundRow("WAIT_MEASURE"));
|
||||
when(mapper.insertSnapshot(anyLong(), anyLong(), anyString(), anyInt(), anyString(), any(), any(),
|
||||
anyString(), anyString(), anyString(), anyString(), any())).thenReturn(1);
|
||||
when(mapper.insertConsumedEvent(anyLong(), anyString(), anyLong(), anyInt(), anyString())).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(inboundRow("WAIT_MEASURE"));
|
||||
when(mapper.lockSnapshot(10001L, 77L)).thenReturn(
|
||||
new OrderDeclarationSyncMapper.SnapshotRow(10001L, 77L, "event-5", 5, "hash"));
|
||||
when(mapper.insertConsumedEvent(anyLong(), anyString(), anyLong(), anyInt(), anyString())).thenReturn(1);
|
||||
|
||||
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());
|
||||
verify(mapper).insertConsumedEvent(anyLong(), anyString(), anyLong(), anyInt(), anyString());
|
||||
}
|
||||
|
||||
@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(inboundRow("WAIT_MEASURE"));
|
||||
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 OrderDeclarationSyncMapper.InboundRow inboundRow(String status) {
|
||||
return new OrderDeclarationSyncMapper.InboundRow(
|
||||
88L, 77L, "FL-77", "RK-77", status, "MATSON_OLD");
|
||||
}
|
||||
|
||||
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")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user