优化
This commit is contained in:
+52
@@ -0,0 +1,52 @@
|
||||
package com.baiyue.order.controller;
|
||||
|
||||
import com.baiyue.order.dto.OrderDeclarationDtos;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class OrderDeclarationUpdateContractTest {
|
||||
@Test
|
||||
void endpointUsesDedicatedPermissionAndVersionedDto() throws Exception {
|
||||
var method = PlatformUnifiedOrderController.class.getDeclaredMethod("updateDeclaration",
|
||||
com.baiyue.common.security.AuthenticatedUser.class, long.class, long.class,
|
||||
OrderDeclarationDtos.UpdateRequest.class);
|
||||
assertThat(method.getAnnotation(PutMapping.class).value()).containsExactly("/{orderId}/declaration");
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('platform:order:declaration:edit')");
|
||||
assertThat(OrderDeclarationDtos.UpdateRequest.class.getRecordComponents())
|
||||
.extracting(component -> component.getName())
|
||||
.containsExactly("expectedVersion", "customerRemark", "expectedArrivalAt",
|
||||
"channelCode", "channelName", "cargoes");
|
||||
assertThat(OrderDeclarationDtos.Cargo.class.getRecordComponents())
|
||||
.extracting(component -> component.getName())
|
||||
.contains("itemNo", "cargoName", "cargoNameEn", "skuNo", "model", "packageCount",
|
||||
"totalQuantity", "lengthCm", "widthCm", "heightCm", "grossWeightKg", "volumeCbm")
|
||||
.doesNotContain("id", "orderId", "actualGrossWeightKg", "actualVolumeCbm",
|
||||
"measurementReviewStatus", "feeReviewStatus");
|
||||
}
|
||||
|
||||
@Test
|
||||
void openApiSeparatesDeclaredAndMeasuredFacts() throws Exception {
|
||||
String source = new ClassPathResource("openapi/order-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Map<String, Object> document = map(new Yaml().load(source));
|
||||
assertThat(map(document.get("paths")))
|
||||
.containsKey("/api/platform/v1/orders/{orderId}/declaration");
|
||||
Map<String, Object> schemas = map(map(document.get("components")).get("schemas"));
|
||||
Map<String, Object> row = map(map(schemas.get("UnifiedOrderRow")).get("properties"));
|
||||
assertThat(row).containsKeys("expectedVolumeCbm", "actualVolumeCbm", "expectedArrivalAt", "channelChanged");
|
||||
assertThat(map(row.get("actualVolumeCbm")).get("description").toString()).contains("仅 Firstleg 实测");
|
||||
assertThat(source).contains("ORDER_DECLARATION_UPDATED_V1", "platform:order:declaration:edit", "A0409");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> map(Object value) { return (Map<String, Object>) value; }
|
||||
}
|
||||
+1
@@ -52,6 +52,7 @@ class DomesticWarehouseOrderExportProviderServiceTest {
|
||||
.doesNotContainKeys("orderId", "tenantId", "customerId", "inboundId",
|
||||
"receivingWarehouseId", "destinationWarehouseId");
|
||||
assertThat(response.columns()).extracting(column -> column.key())
|
||||
.contains("expectedVolumeCbm", "expectedArrivalAt", "channelChanged")
|
||||
.doesNotContain("orderId", "tenantId", "customerId", "inboundId");
|
||||
verify(orders, never()).platformPage(any(), any());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.dto.OrderDeclarationDtos;
|
||||
import com.baiyue.order.dto.UnifiedOrderResponse;
|
||||
import com.baiyue.order.entity.OrderCargoEntity;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.entity.OrderStatus;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
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.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
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.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(OrderDeclarationUpdateService.class)
|
||||
class OrderDeclarationUpdateServiceTest {
|
||||
@MockBean private OrderRepository repository;
|
||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||
@MockBean private UnifiedOrderQueryService unifiedOrders;
|
||||
@MockBean private ObjectMapper json;
|
||||
@Resource private OrderDeclarationUpdateService service;
|
||||
|
||||
@Test
|
||||
void updatesCompleteCustomerDeclarationByItemNoAndKeepsInboundCode() {
|
||||
OrderEntity order = order(OrderStatus.SUBMITTED, 3);
|
||||
OrderCargoEntity cargo = cargo();
|
||||
when(repository.findOwned(100L, 200L)).thenReturn(order);
|
||||
when(repository.findCargo(100L, 200L)).thenReturn(List.of(cargo));
|
||||
when(repository.updateDeclarationHeader(eq(100L), eq(200L), eq(3), eq(700L),
|
||||
any(), any(), anyString(), anyString(), anyBoolean())).thenReturn(1);
|
||||
when(repository.updateDeclarationCargo(eq(100L), eq(200L), any())).thenReturn(1);
|
||||
|
||||
var request = request(3, "NEW", "新渠道", "新备注");
|
||||
var response = service.update(100L, 200L, 700L, "平台操作员", request);
|
||||
|
||||
assertThat(response.orderNo()).isEqualTo("ORD-200");
|
||||
assertThat(response.inboundCode()).isEqualTo("RK-LOCKED");
|
||||
assertThat(response.version()).isEqualTo(4);
|
||||
assertThat(response.channelChanged()).isTrue();
|
||||
assertThat(response.changes()).extracting(OrderDeclarationDtos.FieldChange::field)
|
||||
.contains("channelCode", "channelName", "customerRemark", "expectedArrivalAt",
|
||||
"cargoName", "packageCount", "totalQuantity", "lengthCm", "grossWeightKg");
|
||||
verify(pricingChannelGuard).requireAvailable(100L, 31L, "NEW", "FBA");
|
||||
verify(repository).insertHistory(any());
|
||||
verify(repository, never()).insertOutbox(any());
|
||||
assertThat(cargo.getPackageCount()).isEqualTo(20);
|
||||
assertThat(cargo.getTotalQuantity()).isEqualTo(200);
|
||||
assertThat(cargo.getLengthCm()).isEqualByComparingTo("60");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleVersionBeforeAnyWrite() {
|
||||
when(repository.findOwned(100L, 200L)).thenReturn(order(OrderStatus.SUBMITTED, 4));
|
||||
assertThatThrownBy(() -> service.update(100L, 200L, 700L, "平台操作员",
|
||||
request(3, "OLD", "旧渠道", "备注")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("版本已变化");
|
||||
verify(repository, never()).updateDeclarationHeader(anyLong(), anyLong(), anyInt(), anyLong(),
|
||||
any(), any(), any(), any(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedOrderWithMeasurementFactIsAtomicallyRejected() {
|
||||
OrderEntity order = order(OrderStatus.APPROVED, 3);
|
||||
when(repository.findOwned(100L, 200L)).thenReturn(order);
|
||||
when(repository.findCargo(100L, 200L)).thenReturn(List.of(cargo()));
|
||||
UnifiedOrderResponse.Detail detail = org.mockito.Mockito.mock(UnifiedOrderResponse.Detail.class);
|
||||
UnifiedOrderResponse.Row row = org.mockito.Mockito.mock(UnifiedOrderResponse.Row.class);
|
||||
when(detail.summary()).thenReturn(row);
|
||||
when(row.mainStatus()).thenReturn("WAIT_MEASURE");
|
||||
when(row.measurementCompletedAt()).thenReturn(LocalDateTime.of(2026, 9, 1, 12, 0));
|
||||
when(unifiedOrders.platformDetail(100L, 200L)).thenReturn(detail);
|
||||
|
||||
assertThatThrownBy(() -> service.update(100L, 200L, 700L, "平台操作员",
|
||||
request(3, "OLD", "旧渠道", "备注")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("测量、分柜、装柜或计费事实");
|
||||
verify(repository, never()).updateDeclarationHeader(anyLong(), anyLong(), anyInt(), anyLong(),
|
||||
any(), any(), any(), any(), anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reversibleApprovedUpdateProducesVersionedOutboxSnapshot() throws Exception {
|
||||
OrderEntity order = order(OrderStatus.APPROVED, 3);
|
||||
when(repository.findOwned(100L, 200L)).thenReturn(order);
|
||||
when(repository.findCargo(100L, 200L)).thenReturn(List.of(cargo()));
|
||||
when(repository.updateDeclarationHeader(anyLong(), anyLong(), anyInt(), anyLong(), any(), any(),
|
||||
any(), any(), anyBoolean())).thenReturn(1);
|
||||
when(repository.updateDeclarationCargo(anyLong(), anyLong(), any())).thenReturn(1);
|
||||
UnifiedOrderResponse.Detail detail = org.mockito.Mockito.mock(UnifiedOrderResponse.Detail.class);
|
||||
UnifiedOrderResponse.Row row = org.mockito.Mockito.mock(UnifiedOrderResponse.Row.class);
|
||||
when(detail.summary()).thenReturn(row);
|
||||
when(row.mainStatus()).thenReturn("WAIT_MEASURE");
|
||||
when(row.feeReviewStatus()).thenReturn("UNRECORDED");
|
||||
when(unifiedOrders.platformDetail(100L, 200L)).thenReturn(detail);
|
||||
when(json.writeValueAsString(any())).thenReturn("{\"schemaVersion\":1}");
|
||||
|
||||
service.update(100L, 200L, 700L, "平台操作员", request(3, "OLD", "旧渠道", "新备注"));
|
||||
|
||||
verify(repository).insertOutbox(any());
|
||||
}
|
||||
|
||||
private OrderEntity order(OrderStatus status, int version) {
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(200L); order.setTenantId(100L); order.setOrderNo("ORD-200");
|
||||
order.setInboundCode("RK-LOCKED"); order.setStatus(status); order.setVersion(version);
|
||||
order.setReceivingWarehouseId(31L); order.setDestinationType("FBA");
|
||||
order.setChannelCode("OLD"); order.setChannelName("旧渠道"); order.setChannelChanged(false);
|
||||
order.setCustomerRemark("旧备注");
|
||||
return order;
|
||||
}
|
||||
|
||||
private OrderCargoEntity cargo() {
|
||||
OrderCargoEntity value = new OrderCargoEntity();
|
||||
value.setId(300L); value.setTenantId(100L); value.setOrderId(200L); value.setItemNo(1); value.setVersion(2);
|
||||
value.setCargoName("旧品名"); value.setCargoNameEn("Old"); value.setSkuNo("SKU-OLD"); value.setModel("M1");
|
||||
value.setPackageCount(10); value.setTotalQuantity(100); value.setLengthCm(new BigDecimal("50"));
|
||||
value.setWidthCm(new BigDecimal("40")); value.setHeightCm(new BigDecimal("30"));
|
||||
value.setGrossWeightKg(new BigDecimal("100")); value.setVolumeCbm(new BigDecimal("1.0"));
|
||||
return value;
|
||||
}
|
||||
|
||||
private OrderDeclarationDtos.UpdateRequest request(int version, String channelCode,
|
||||
String channelName, String remark) {
|
||||
var cargo = new OrderDeclarationDtos.Cargo(1, "新品名", "New", "SKU-NEW", "M2",
|
||||
20, 200, new BigDecimal("60"), new BigDecimal("50"), new BigDecimal("40"),
|
||||
new BigDecimal("120"), new BigDecimal("1.2"), new BigDecimal("110"),
|
||||
"8518", "851821", 10, new BigDecimal("5"), new BigDecimal("1000"),
|
||||
"USD", "塑料", "音响", "Brand", "新备注");
|
||||
return new OrderDeclarationDtos.UpdateRequest(version, remark,
|
||||
LocalDateTime.of(2026, 9, 15, 10, 30), channelCode, channelName, List.of(cargo));
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,22 @@ class OrderInboundOutboxPublisherTest {
|
||||
verify(fixture.repository, never()).markOutboxFailed(any(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void declarationUpdateEventIsDeliveredByVersionedContract() {
|
||||
String payload = "{\"schemaVersion\":1,\"eventId\":\"evt-1\",\"tenantId\":11,"
|
||||
+ "\"orderId\":21,\"orderNo\":\"ORD-21\",\"inboundCode\":\"RK-21\","
|
||||
+ "\"version\":4,\"operatorId\":7,\"channelCode\":\"SEA\","
|
||||
+ "\"channelName\":\"海运\",\"channelChanged\":true,\"cargoes\":[],\"changes\":[]}";
|
||||
Fixture fixture = fixture(event("ORDER_DECLARATION_UPDATED_V1", 11L, 21L, payload));
|
||||
when(fixture.client.updateDeclaration(eq("internal-token"), any())).thenReturn(ApiResponse.success(null));
|
||||
|
||||
fixture.publisher.publishApprovedOrders();
|
||||
|
||||
verify(fixture.client).updateDeclaration(eq("internal-token"), any());
|
||||
verify(fixture.repository).markOutboxPublished(eq(11L), eq(41L), any(LocalDateTime.class));
|
||||
verify(fixture.client, never()).create(any(), any());
|
||||
}
|
||||
|
||||
private Fixture fixture(OutboxEventEntity event) {
|
||||
return fixture(event, "BUY_DECLARATION");
|
||||
}
|
||||
|
||||
@@ -98,9 +98,10 @@ class UnifiedOrderQueryServiceTest {
|
||||
|
||||
assertThat(service.page(10001L, query).records()).singleElement().satisfies(row -> {
|
||||
assertThat(row.totalPackageCount()).isEqualTo(100);
|
||||
assertThat(row.actualGrossWeightKg()).isEqualByComparingTo("12.5");
|
||||
assertThat(row.totalNetWeightKg()).isEqualByComparingTo("11.2");
|
||||
assertThat(row.actualVolumeCbm()).isEqualByComparingTo("0.375");
|
||||
assertThat(row.expectedVolumeCbm()).isEqualByComparingTo("0.375");
|
||||
assertThat(row.actualGrossWeightKg()).isNull();
|
||||
assertThat(row.totalNetWeightKg()).isNull();
|
||||
assertThat(row.actualVolumeCbm()).isNull();
|
||||
});
|
||||
verify(orders).findCargoByOrderIds(10001L, List.of(77L));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user