feat(order): add customer order foundations

This commit is contained in:
2026-09-01 19:17:43 +08:00
parent 222c3790dc
commit caffcb90a9
24 changed files with 992 additions and 49 deletions
@@ -0,0 +1,26 @@
package com.baiyue.order.controller;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.assertj.core.api.Assertions.assertThat;
class OrderCustomerFoundationOpenApiContractTest {
@Test
void publishesAddressBatchAndWoodInspectionContracts() throws Exception {
try (var input = getClass().getResourceAsStream("/openapi/order-v1.yaml")) {
assertThat(input).isNotNull();
String yaml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
assertThat(yaml)
.contains("/api/merchant/v1/order-addresses:")
.contains("/api/merchant/v1/orders/first-leg/batch-imports:")
.contains("senderAddressId:")
.contains("privateDestinationAddressId:")
.contains("woodenProduct:")
.contains("ORDER_BATCH_IMPORT_SOURCE")
.contains("ATOMIC 在任一行失败时整批回滚")
.doesNotContain("发货单位和收货单位仅通过 sender/receiver");
}
}
}
@@ -0,0 +1,41 @@
package com.baiyue.order.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.dto.CustomerAddressDtos;
import com.baiyue.order.entity.CustomerAddressEntity;
import com.baiyue.order.mapper.CustomerAddressMapper;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class CustomerAddressServiceTest {
@Test
void createsCustomerOwnedPrivateAddressWithoutCallingWarehouse() {
CustomerAddressMapper mapper = mock(CustomerAddressMapper.class);
CustomerAddressService service = new CustomerAddressService();
ReflectionTestUtils.setField(service, "mapper", mapper);
var request = new CustomerAddressDtos.SaveRequest("PRIVATE_DESTINATION", "洛杉矶私人仓", "客户海外仓",
"Alex", "+1-213-555-0100", null, "US", "CA", "Los Angeles", null,
"123 Private Ave", "90001", false);
service.create(10001L, 20001L, request);
verify(mapper).insert(any(CustomerAddressEntity.class));
}
@Test
void selectingAnotherCustomersAddressIsReportedAsNotFound() {
CustomerAddressMapper mapper = mock(CustomerAddressMapper.class);
when(mapper.selectOne(any())).thenReturn(null);
CustomerAddressService service = new CustomerAddressService();
ReflectionTestUtils.setField(service, "mapper", mapper);
assertThatThrownBy(() -> service.required(10001L, 90001L, "PICKUP"))
.isInstanceOf(BizException.class).hasMessage("客户地址不存在或已停用");
}
}
@@ -21,6 +21,7 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(MerchantBatchDraftService.class)
class MerchantBatchDraftServiceTest {
@@ -85,6 +86,21 @@ class MerchantBatchDraftServiceTest {
assertThat(anotherRow).isNotEqualTo(first);
}
@Test
void atomicModeValidatesEveryRowBeforeCreatingJoinedTransactions() {
when(itemService.createJoined(anyLong(), anyLong(), isNull(), any(), anyString()))
.thenReturn(order(101L, "FL101", null), order(102L, "FL102", null));
var response = service.createAtomic(10L, 20L, null, "atomic-key",
List.of(item("row-1"), item("row-2")));
assertThat(response.succeeded()).isEqualTo(2);
verify(itemService, org.mockito.Mockito.times(2)).validate(any());
verify(itemService, org.mockito.Mockito.times(2))
.createJoined(anyLong(), anyLong(), isNull(), any(), anyString());
verify(itemService, never()).create(anyLong(), anyLong(), isNull(), any(), anyString());
}
@Test
void rejectsInvalidBatchBeforeCreatingRows() {
assertThatThrownBy(() -> service.create(10L, 20L, " ", List.of(item("row-1"))))
@@ -0,0 +1,22 @@
package com.baiyue.order.service;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import static org.assertj.core.api.Assertions.assertThat;
class MerchantBatchImportServiceTest {
@Test
void templateUsesStableRowIdentityAndAuthoritativeDraftSchema() throws Exception {
byte[] content = new MerchantBatchImportService().template();
try (var workbook = new XSSFWorkbook(new ByteArrayInputStream(content))) {
var sheet = workbook.getSheetAt(0);
assertThat(sheet.getRow(0).getCell(0).getStringCellValue()).isEqualTo("clientRowId");
assertThat(sheet.getRow(0).getCell(11).getStringCellValue()).isEqualTo("senderAddressId");
assertThat(sheet.getRow(0).getCell(21).getStringCellValue()).isEqualTo("privateDestinationAddressId");
assertThat(sheet.getRow(0).getCell(32).getStringCellValue()).isEqualTo("cargo.woodenProduct");
}
}
}
@@ -476,29 +476,31 @@ class OrderApplicationServiceStateTest {
}
@Test
void pickupDraftAllowsMissingSender() {
void pickupDraftRequiresSelectedAddress() {
OrderRepository repository = mock(OrderRepository.class);
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
OrderRequests.SaveRequest request = request(null, "PICKUP", null, null, "OVERSEAS");
when(masterData.validate(20002L, request)).thenReturn(snapshots(null));
when(repository.completeIdempotency(anyLong(), any(), anyLong())).thenReturn(1);
service(repository, mock(OrderEligibilityGuard.class), masterData)
.createDraft(20002L, 2L, request, "pickup-without-sender");
verify(repository).insertOrder(any());
assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class), masterData)
.createDraft(20002L, 2L, request, "pickup-without-sender"))
.isInstanceOf(BizException.class).hasMessage("上门提货时必须选择取货地址");
verify(repository, never()).insertOrder(any());
}
@Test
void privateDraftAllowsMissingReceiver() {
void privateDraftRequiresSelectedAddress() {
OrderRepository repository = mock(OrderRepository.class);
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
OrderRequests.SaveRequest request = request(null, "SELF_EXPRESS", null, null, "PRIVATE");
when(masterData.validate(20002L, request)).thenReturn(snapshots(null));
when(repository.completeIdempotency(anyLong(), any(), anyLong())).thenReturn(1);
service(repository, mock(OrderEligibilityGuard.class), masterData)
.createDraft(20002L, 2L, request, "private-without-receiver");
verify(repository).insertOrder(any());
assertThatThrownBy(() -> service(repository, mock(OrderEligibilityGuard.class), masterData)
.createDraft(20002L, 2L, request, "private-without-receiver"))
.isInstanceOf(BizException.class).hasMessage("私人目的仓必须选择客户地址");
verify(repository, never()).insertOrder(any());
}
@Test
@@ -0,0 +1,37 @@
package com.baiyue.order.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.dto.OrderRequests;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class OrderWoodInspectionRuleTest {
@Test
void woodProductRequiresInspectionWithStableMessage() {
OrderRequests.Cargo cargo = mock(OrderRequests.Cargo.class);
OrderRequests.SaveRequest request = mock(OrderRequests.SaveRequest.class);
when(cargo.woodenProduct()).thenReturn(true);
when(request.cargoes()).thenReturn(List.of(cargo));
when(request.customsInspection()).thenReturn(null);
assertThatThrownBy(() -> OrderApplicationService.validateWoodInspection(request))
.isInstanceOf(BizException.class).hasMessage("木制品必须选择商检");
}
@Test
void nonWoodProductAllowsInspectionToBeOmitted() {
OrderRequests.Cargo cargo = mock(OrderRequests.Cargo.class);
OrderRequests.SaveRequest request = mock(OrderRequests.SaveRequest.class);
when(cargo.woodenProduct()).thenReturn(false);
when(request.cargoes()).thenReturn(List.of(cargo));
when(request.customsInspection()).thenReturn(null);
assertThatCode(() -> OrderApplicationService.validateWoodInspection(request)).doesNotThrowAnyException();
}
}