优化
This commit is contained in:
@@ -14,6 +14,6 @@ public interface FirstlegPricingClient {
|
||||
@RequestBody ChannelValidationRequest request);
|
||||
|
||||
record ChannelValidationRequest(long tenantId, long originWarehouseId, String channelCode,
|
||||
String destinationType) {}
|
||||
String destinationType, boolean requireInboundCodePrefix) {}
|
||||
record ChannelValidationResponse(String channelCode, String inboundCodePrefix) {}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.order.dto.BatchOrderReviewResponse;
|
||||
import com.baiyue.order.dto.OrderDetailResponse;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.dto.InboundCodePolicyResponse;
|
||||
import com.baiyue.order.dto.TenantOrderCountResponse;
|
||||
import com.baiyue.order.dto.TenantOrderStatisticsResponse;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
@@ -47,6 +48,12 @@ public class PlatformFirstLegOrderController {
|
||||
@Resource
|
||||
private OrderApplicationService service;
|
||||
|
||||
@GetMapping("/inbound-code-policy")
|
||||
@PreAuthorize("hasAnyAuthority('platform:order:list', 'platform:order:create', 'platform:order:edit', 'platform:order:submit')")
|
||||
public ApiResponse<InboundCodePolicyResponse> inboundCodePolicy() {
|
||||
return ApiResponse.success(service.inboundCodePolicy());
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "ORDER_CREATE", resourceType = "FIRST_LEG_ORDER", description = "平台代客创建草稿")
|
||||
@Operation(summary = "平台代客创建草稿", description = "在明确指定的目标租户内创建头程订单草稿,必须携带幂等键。")
|
||||
@ApiResponses({
|
||||
|
||||
@@ -121,10 +121,9 @@ public class OrderRepository {
|
||||
.eq(OrderEntity::getId, orderId));
|
||||
}
|
||||
|
||||
public boolean inboundCodeExists(long tenantId, String inboundCode, Long excludedOrderId) {
|
||||
public boolean inboundCodeExists(String inboundCode, Long excludedOrderId) {
|
||||
if (!StringUtils.hasText(inboundCode)) return false;
|
||||
return orderMapper.selectCount(new LambdaQueryWrapper<OrderEntity>()
|
||||
.eq(OrderEntity::getTenantId, tenantId)
|
||||
.eq(OrderEntity::getInboundCode, inboundCode)
|
||||
.ne(excludedOrderId != null, OrderEntity::getId, excludedOrderId)) > 0;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ package com.baiyue.order.service;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Validator;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -13,9 +16,19 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class MerchantBatchDraftItemService {
|
||||
@Resource private OrderApplicationService orders;
|
||||
@Resource private OrderRepository repository;
|
||||
@Resource private Validator validator;
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public OrderEntity create(long tenantId, long userId, OrderRequests.SaveRequest draft, String idempotencyKey) {
|
||||
var violations = validator.validate(draft);
|
||||
if (!violations.isEmpty()) {
|
||||
String message = violations.stream()
|
||||
.sorted(java.util.Comparator.comparing(violation -> violation.getPropertyPath().toString()))
|
||||
.findFirst()
|
||||
.map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
|
||||
.orElse("草稿参数不合法");
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, message);
|
||||
}
|
||||
Long id = orders.createDraft(tenantId, userId, draft, idempotencyKey);
|
||||
return repository.findOwned(tenantId, id);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,11 @@ public class OrderApplicationService {
|
||||
order.setStatus(OrderStatus.DRAFT);
|
||||
apply(order, tenantId, request, snapshots);
|
||||
order.setCreatedBy(userId); order.setUpdatedBy(userId);
|
||||
repository.insertOrder(order);
|
||||
try {
|
||||
repository.insertOrder(order);
|
||||
} catch (DuplicateKeyException duplicate) {
|
||||
throw duplicateInboundCode(order, duplicate);
|
||||
}
|
||||
saveChildren(tenantId, userId, order.getId(), request);
|
||||
record(tenantId, order.getId(), null, OrderStatus.DRAFT, "CREATE", null, userId, null);
|
||||
return order.getId();
|
||||
@@ -118,7 +122,11 @@ public class OrderApplicationService {
|
||||
if (before == OrderStatus.SUBMITTED) {
|
||||
order.setStatus(OrderStatus.SUBMITTED);
|
||||
}
|
||||
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
|
||||
try {
|
||||
if (repository.saveOrder(tenantId, order) != 1) throw new BizException("Concurrent order update");
|
||||
} catch (DuplicateKeyException duplicate) {
|
||||
throw duplicateInboundCode(order, duplicate);
|
||||
}
|
||||
repository.replaceDraftChildren(tenantId, id);
|
||||
saveChildren(tenantId, userId, id, request);
|
||||
record(tenantId, id, before, order.getStatus(), "UPDATE", null, userId, null);
|
||||
@@ -473,7 +481,7 @@ public class OrderApplicationService {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (normalized != null && repository.inboundCodeExists(order.getTenantId(), normalized, order.getId())) {
|
||||
if (normalized != null && repository.inboundCodeExists(normalized, order.getId())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "入仓码已存在,请核对旧系统数据");
|
||||
}
|
||||
order.setInboundCode(normalized);
|
||||
@@ -489,6 +497,12 @@ public class OrderApplicationService {
|
||||
}
|
||||
return pricingChannelGuard.requireAvailable(tenantId, receivingWarehouseId, channelCode, destinationType);
|
||||
}
|
||||
private RuntimeException duplicateInboundCode(OrderEntity order, DuplicateKeyException duplicate) {
|
||||
if (StringUtils.hasText(order.getInboundCode())) {
|
||||
return new BizException(ResultCode.CONFLICT, "入仓码已存在,请核对旧系统数据");
|
||||
}
|
||||
return duplicate;
|
||||
}
|
||||
private void requirePrintableInboundCode(OrderEntity order) {
|
||||
String inboundCode = order.getInboundCode();
|
||||
if (!StringUtils.hasText(inboundCode) || !inboundCode.matches("[A-Za-z0-9._:-]+")) {
|
||||
|
||||
@@ -16,10 +16,20 @@ public class OrderPricingChannelGuard {
|
||||
|
||||
public String requireAvailable(long tenantId, long originWarehouseId, String channelCode,
|
||||
String destinationType) {
|
||||
return validate(tenantId, originWarehouseId, channelCode, destinationType, true);
|
||||
}
|
||||
|
||||
public void requireActive(long tenantId, long originWarehouseId, String channelCode,
|
||||
String destinationType) {
|
||||
validate(tenantId, originWarehouseId, channelCode, destinationType, false);
|
||||
}
|
||||
|
||||
private String validate(long tenantId, long originWarehouseId, String channelCode,
|
||||
String destinationType, boolean requireInboundCodePrefix) {
|
||||
try {
|
||||
ApiResponse<FirstlegPricingClient.ChannelValidationResponse> response = client.validateChannel(internalToken,
|
||||
new FirstlegPricingClient.ChannelValidationRequest(
|
||||
tenantId, originWarehouseId, channelCode, destinationType));
|
||||
tenantId, originWarehouseId, channelCode, destinationType, requireInboundCodePrefix));
|
||||
if (response == null) {
|
||||
throw unavailable();
|
||||
}
|
||||
@@ -29,8 +39,8 @@ public class OrderPricingChannelGuard {
|
||||
? code.message() : response.message();
|
||||
throw new BizException(code, message);
|
||||
}
|
||||
if (response.data() == null || response.data().inboundCodePrefix() == null
|
||||
|| response.data().inboundCodePrefix().isBlank()) {
|
||||
if (response.data() == null || requireInboundCodePrefix
|
||||
&& (response.data().inboundCodePrefix() == null || response.data().inboundCodePrefix().isBlank())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置入仓码前缀");
|
||||
}
|
||||
return response.data().inboundCodePrefix();
|
||||
|
||||
@@ -9,7 +9,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '订单ID(雪花ID)',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '客户范围内唯一订单号',
|
||||
inbound_code VARCHAR(40) NULL COMMENT '提交后生成并固定的入仓码;未提交草稿为空',
|
||||
inbound_code VARCHAR(40) NULL COMMENT '全平台唯一入仓码;来源由全局模式决定,提交后固定',
|
||||
customer_id BIGINT UNSIGNED NULL COMMENT '客户逻辑ID',
|
||||
customer_name VARCHAR(128) NOT NULL COMMENT '下单时客户名称快照',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订单业务状态:DRAFT草稿、SUBMITTED已提交、APPROVED审核通过、ACCEPTED已接单、REJECTED审核驳回、PROCESSING履约中、COMPLETED已完成、CANCELLED已取消',
|
||||
@@ -54,13 +54,40 @@ CREATE TABLE IF NOT EXISTS ord_order (
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_order_tenant_no (tenant_id, order_no),
|
||||
UNIQUE KEY uk_order_tenant_inbound_code (tenant_id, inbound_code),
|
||||
UNIQUE KEY uk_order_inbound_code (inbound_code),
|
||||
KEY idx_order_tenant_status_time (tenant_id, status, created_at),
|
||||
KEY idx_order_tenant_role_submitted (tenant_id, order_role, submitted_at),
|
||||
KEY idx_order_tenant_customer (tenant_id, customer_id),
|
||||
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
|
||||
) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态';
|
||||
|
||||
-- 既有开发库必须先确认不存在跨客户重复码;若存在则在此处失败,不自动改写业务数据。
|
||||
SET @order_global_inbound_index_sql = IF(
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||
AND index_name = 'uk_order_inbound_code'
|
||||
),
|
||||
'ALTER TABLE baiyue_order.ord_order ADD UNIQUE KEY uk_order_inbound_code (inbound_code)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE order_global_inbound_index_stmt FROM @order_global_inbound_index_sql;
|
||||
EXECUTE order_global_inbound_index_stmt;
|
||||
DEALLOCATE PREPARE order_global_inbound_index_stmt;
|
||||
|
||||
SET @order_tenant_inbound_index_drop_sql = IF(
|
||||
EXISTS(
|
||||
SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||
AND index_name = 'uk_order_tenant_inbound_code'
|
||||
),
|
||||
'ALTER TABLE baiyue_order.ord_order DROP INDEX uk_order_tenant_inbound_code',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE order_tenant_inbound_index_drop_stmt FROM @order_tenant_inbound_index_drop_sql;
|
||||
EXECUTE order_tenant_inbound_index_drop_stmt;
|
||||
DEALLOCATE PREPARE order_tenant_inbound_index_drop_stmt;
|
||||
|
||||
-- Existing development databases may still have the former NOT NULL contract.
|
||||
SET @order_inbound_code_nullable_sql = IF(
|
||||
EXISTS(
|
||||
@@ -68,7 +95,7 @@ SET @order_inbound_code_nullable_sql = IF(
|
||||
WHERE table_schema = 'baiyue_order' AND table_name = 'ord_order'
|
||||
AND column_name = 'inbound_code' AND is_nullable = 'NO'
|
||||
),
|
||||
'ALTER TABLE baiyue_order.ord_order MODIFY COLUMN inbound_code VARCHAR(40) NULL COMMENT ''提交后生成并固定的入仓码;未提交草稿为空''',
|
||||
'ALTER TABLE baiyue_order.ord_order MODIFY COLUMN inbound_code VARCHAR(40) NULL COMMENT ''全平台唯一入仓码;来源由全局模式决定,提交后固定''',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE order_inbound_code_nullable_stmt FROM @order_inbound_code_nullable_sql;
|
||||
|
||||
@@ -6,6 +6,21 @@ info:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/api/platform/v1/orders/first-leg/inbound-code-policy:
|
||||
get:
|
||||
summary: 平台端查询全系统入仓码模式
|
||||
description: 与客户下单端返回同一份全局策略,不按客户区分。
|
||||
responses:
|
||||
'200':
|
||||
description: 查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string, example: '200'}
|
||||
data: {$ref: '#/components/schemas/InboundCodePolicy'}
|
||||
'403': {description: 需要平台订单列表、创建、编辑或提交权限之一}
|
||||
/api/platform/v1/orders:
|
||||
get:
|
||||
summary: 平台统一订单列表
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.order.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class InboundCodeModeProviderTest {
|
||||
@Test
|
||||
void parsesBothSupportedModesCaseInsensitively() {
|
||||
assertThat(new InboundCodeModeProvider("auto").mode())
|
||||
.isEqualTo(InboundCodeModeProvider.Mode.AUTO);
|
||||
assertThat(new InboundCodeModeProvider(" legacy_manual ").legacyManual()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingAndUnknownModesInsteadOfFallingBackToAutomaticGeneration() {
|
||||
assertThatThrownBy(() -> new InboundCodeModeProvider(" "))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("baiyue.order.inbound-code-mode 必须显式配置");
|
||||
assertThatThrownBy(() -> new InboundCodeModeProvider("LEGACY-MANUAL"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("baiyue.order.inbound-code-mode 仅支持 AUTO 或 LEGACY_MANUAL");
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,12 @@ class InboundCodePolicyContractTest {
|
||||
.isEqualTo("hasAnyAuthority('merchant:order:list', 'merchant:order:create', 'merchant:order:edit')");
|
||||
assertThat(method.getGenericReturnType().getTypeName())
|
||||
.contains(InboundCodePolicyResponse.class.getName());
|
||||
|
||||
var platformMethod = PlatformFirstLegOrderController.class.getDeclaredMethod("inboundCodePolicy");
|
||||
assertThat(platformMethod.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAnyAuthority('platform:order:list', 'platform:order:create', 'platform:order:edit', 'platform:order:submit')");
|
||||
assertThat(platformMethod.getGenericReturnType().getTypeName())
|
||||
.contains(InboundCodePolicyResponse.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -29,6 +35,7 @@ class InboundCodePolicyContractTest {
|
||||
|
||||
assertThat(openapi)
|
||||
.contains("/api/merchant/v1/orders/first-leg/inbound-code-policy:")
|
||||
.contains("/api/platform/v1/orders/first-leg/inbound-code-policy:")
|
||||
.contains("enum: [AUTO, LEGACY_MANUAL]")
|
||||
.contains("manualEntryRequired: {type: boolean}")
|
||||
.contains("该策略不按客户区分")
|
||||
|
||||
@@ -54,6 +54,32 @@ class OrderRequestsValidationTest {
|
||||
assertThat(validator.validate(draft)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesTheManuallyEnteredInboundCodeForSingleAndBatchCallers() {
|
||||
var source = request(null, null, "TAX_INCLUDED");
|
||||
var invalid = withInboundCode(source, "INVALID#CODE");
|
||||
var tooLong = withInboundCode(source, "A".repeat(41));
|
||||
var valid = withInboundCode(source, "NYEXX2608140001");
|
||||
|
||||
assertThat(validator.validate(invalid))
|
||||
.anyMatch(violation -> violation.getPropertyPath().toString().equals("inboundCode"));
|
||||
assertThat(validator.validate(tooLong))
|
||||
.anyMatch(violation -> violation.getPropertyPath().toString().equals("inboundCode"));
|
||||
assertThat(validator.validate(valid)).isEmpty();
|
||||
}
|
||||
|
||||
private OrderRequests.SaveRequest withInboundCode(OrderRequests.SaveRequest source, String inboundCode) {
|
||||
return new OrderRequests.SaveRequest(
|
||||
source.customerId(), source.customerName(), source.transportMode(), source.tradeTerm(),
|
||||
source.destinationCountryCode(), source.destinationWarehouseId(), source.destinationWarehouseName(),
|
||||
source.expectedShipDate(), source.customerRemark(), source.sender(), source.receiver(), source.cargoes(),
|
||||
source.pickupMethod(), source.requestedPickupAt(), source.receivingWarehouseId(),
|
||||
source.receivingWarehouseName(), source.channelCode(), source.channelName(), source.carrierId(),
|
||||
source.carrierName(), source.taxMode(), source.pricingTaxMode(), source.containerMode(),
|
||||
source.customsInspection(), source.destinationType(), source.amazonWarehouseCode(), source.shipmentId(),
|
||||
source.referenceId(), inboundCode);
|
||||
}
|
||||
|
||||
private OrderRequests.SaveRequest request(Long carrierId, String carrierName, String pricingTaxMode) {
|
||||
var cargo = new OrderRequests.Cargo(
|
||||
"货物", null, "CARTON", 1, BigDecimal.ONE, BigDecimal.ONE,
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.order.repository;
|
||||
|
||||
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 InboundCodeGlobalUniquenessContractTest {
|
||||
@Test
|
||||
void orderSchemaUsesOnePlatformWideInboundCodeIndex() throws Exception {
|
||||
String sql = new ClassPathResource("db/order.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("UNIQUE KEY uk_order_inbound_code (inbound_code)")
|
||||
.doesNotContain("UNIQUE KEY uk_order_tenant_inbound_code (tenant_id, inbound_code)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Path;
|
||||
import jakarta.validation.Validator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MerchantBatchDraftItemServiceTest {
|
||||
@Test
|
||||
void rejectsAnInvalidBatchRowBeforeCreatingTheDraft() {
|
||||
OrderApplicationService orders = mock(OrderApplicationService.class);
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
Validator validator = mock(Validator.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
ConstraintViolation<OrderRequests.SaveRequest> violation = mock(ConstraintViolation.class);
|
||||
Path path = mock(Path.class);
|
||||
OrderRequests.SaveRequest draft = mock(OrderRequests.SaveRequest.class);
|
||||
when(path.toString()).thenReturn("inboundCode");
|
||||
when(violation.getPropertyPath()).thenReturn(path);
|
||||
when(violation.getMessage()).thenReturn("格式不正确");
|
||||
when(validator.validate(draft)).thenReturn(Set.of(violation));
|
||||
MerchantBatchDraftItemService service = new MerchantBatchDraftItemService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
ReflectionTestUtils.setField(service, "validator", validator);
|
||||
|
||||
assertThatThrownBy(() -> service.create(1L, 2L, draft, "batch-key"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("inboundCode: 格式不正确");
|
||||
|
||||
verify(orders, never()).createDraft(1L, 2L, draft, "batch-key");
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.order.service;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.client.WarehouseClient;
|
||||
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.entity.OrderEntity;
|
||||
import com.baiyue.order.entity.OrderCargoEntity;
|
||||
@@ -37,6 +38,7 @@ class OrderApplicationServiceStateTest {
|
||||
@MockBean private OrderMasterDataGuard contextMasterData;
|
||||
@MockBean private OrderPricingChannelGuard contextPricingChannelGuard;
|
||||
@MockBean private ProductImageGuard contextProductImageGuard;
|
||||
@MockBean private InboundCodeModeProvider contextInboundCodeModeProvider;
|
||||
@Resource private OrderApplicationService springService;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@@ -260,7 +262,7 @@ class OrderApplicationServiceStateTest {
|
||||
when(masterData.validate(10001L, request)).thenReturn(snapshots());
|
||||
when(repository.completeIdempotency(anyLong(), any(), anyLong())).thenReturn(1);
|
||||
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class), masterData);
|
||||
ReflectionTestUtils.setField(service, "inboundCodeMode", "LEGACY_MANUAL");
|
||||
useInboundCodeMode(service, InboundCodeModeProvider.Mode.LEGACY_MANUAL);
|
||||
|
||||
service.createDraft(10001L, 2L, request, "legacy-code-draft");
|
||||
|
||||
@@ -268,6 +270,38 @@ class OrderApplicationServiceStateTest {
|
||||
verify(repository, never()).nextInboundCodeSequence(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyManualModeRejectsAnInboundCodeAlreadyUsedByAnotherCustomer() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
|
||||
OrderRequests.SaveRequest request = withInboundCode(request(), "NYEXX2608140001");
|
||||
when(masterData.validate(10001L, request)).thenReturn(snapshots());
|
||||
when(repository.inboundCodeExists(eq("NYEXX2608140001"), anyLong())).thenReturn(true);
|
||||
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class), masterData);
|
||||
useInboundCodeMode(service, InboundCodeModeProvider.Mode.LEGACY_MANUAL);
|
||||
|
||||
assertThatThrownBy(() -> service.createDraft(10001L, 2L, request, "duplicate-global-code"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("入仓码已存在,请核对旧系统数据");
|
||||
|
||||
verify(repository, never()).insertOrder(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void automaticModeRejectsClientSuppliedInboundCodes() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
OrderMasterDataGuard masterData = mock(OrderMasterDataGuard.class);
|
||||
OrderRequests.SaveRequest request = withInboundCode(request(), "NYEXX2608140001");
|
||||
when(masterData.validate(10001L, request)).thenReturn(snapshots());
|
||||
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class), masterData);
|
||||
|
||||
assertThatThrownBy(() -> service.createDraft(10001L, 2L, request, "auto-manual-code"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("当前系统由新系统自动生成入仓码,请勿人工填写");
|
||||
|
||||
verify(repository, never()).insertOrder(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyManualModeRejectsSubmissionWithoutAnExternalInboundCode() {
|
||||
OrderRepository repository = mock(OrderRepository.class);
|
||||
@@ -277,7 +311,7 @@ class OrderApplicationServiceStateTest {
|
||||
when(repository.findParties(20002L, 1L)).thenReturn(List.of());
|
||||
OrderApplicationService service = service(repository, mock(OrderEligibilityGuard.class),
|
||||
mock(OrderMasterDataGuard.class));
|
||||
ReflectionTestUtils.setField(service, "inboundCodeMode", "LEGACY_MANUAL");
|
||||
useInboundCodeMode(service, InboundCodeModeProvider.Mode.LEGACY_MANUAL);
|
||||
|
||||
assertThatThrownBy(() -> service.transit(
|
||||
20002L, 2L, "u", 1L, OrderStatus.SUBMITTED, null))
|
||||
@@ -548,10 +582,18 @@ class OrderApplicationServiceStateTest {
|
||||
when(repository.nextInboundCodeSequence(any(LocalDate.class))).thenReturn(1L);
|
||||
ReflectionTestUtils.setField(service, "pricingChannelGuard", pricing);
|
||||
ReflectionTestUtils.setField(service, "productImageGuard", mock(ProductImageGuard.class));
|
||||
useInboundCodeMode(service, InboundCodeModeProvider.Mode.AUTO);
|
||||
ReflectionTestUtils.setField(service, "json", new ObjectMapper().findAndRegisterModules());
|
||||
return service;
|
||||
}
|
||||
|
||||
private void useInboundCodeMode(OrderApplicationService service, InboundCodeModeProvider.Mode mode) {
|
||||
InboundCodeModeProvider provider = mock(InboundCodeModeProvider.class);
|
||||
when(provider.mode()).thenReturn(mode);
|
||||
when(provider.legacyManual()).thenReturn(mode == InboundCodeModeProvider.Mode.LEGACY_MANUAL);
|
||||
ReflectionTestUtils.setField(service, "inboundCodeModeProvider", provider);
|
||||
}
|
||||
|
||||
private OrderRequests.SaveRequest request() {
|
||||
var party = new OrderRequests.Party(
|
||||
"公司", "联系人", "13800138000", null, "CN", null, "深圳", null, "地址", null);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.order.service;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||
import com.baiyue.order.entity.OrderIdempotencyRecordEntity;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -41,6 +42,7 @@ class OrderIdempotencyServiceTest {
|
||||
@MockBean private OrderMasterDataGuard contextMasterData;
|
||||
@MockBean private OrderPricingChannelGuard contextPricingChannelGuard;
|
||||
@MockBean private ProductImageGuard contextProductImageGuard;
|
||||
@MockBean private InboundCodeModeProvider contextInboundCodeModeProvider;
|
||||
@Resource private OrderApplicationService springService;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
@@ -31,7 +31,7 @@ class OrderPricingChannelGuardTest {
|
||||
|
||||
@Test
|
||||
void forwardsCustomerOriginAndChannelToFirstleg() {
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA");
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA", true);
|
||||
when(client.validateChannel("token", request)).thenReturn(ApiResponse.success(
|
||||
new FirstlegPricingClient.ChannelValidationResponse("MATSON_TRUCK", "ZBYHK")));
|
||||
|
||||
@@ -40,9 +40,21 @@ class OrderPricingChannelGuardTest {
|
||||
verify(client).validateChannel("token", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyManualModeOnlyRequiresTheChannelToRemainActive() {
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(
|
||||
20002L, 8L, "LEGACY_CHANNEL", "FBA", false);
|
||||
when(client.validateChannel("token", request)).thenReturn(ApiResponse.success(
|
||||
new FirstlegPricingClient.ChannelValidationResponse("LEGACY_CHANNEL", null)));
|
||||
|
||||
guard.requireActive(20002L, 8L, "LEGACY_CHANNEL", "FBA");
|
||||
|
||||
verify(client).validateChannel("token", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removedOrForgedChannelKeepsFirstlegBusinessError() {
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "FAKE", "FBA");
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "FAKE", "FBA", true);
|
||||
when(client.validateChannel("token", request)).thenReturn(
|
||||
ApiResponse.failure(ResultCode.CONFLICT, "所选渠道已下线,请重新选择", null, null));
|
||||
|
||||
@@ -55,7 +67,7 @@ class OrderPricingChannelGuardTest {
|
||||
|
||||
@Test
|
||||
void nullResponseMapsToServiceUnavailable() {
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA");
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA", true);
|
||||
when(client.validateChannel("token", request)).thenReturn(null);
|
||||
|
||||
assertUnavailable(() -> guard.requireAvailable(20002L, 8L, "MATSON_TRUCK", "FBA"));
|
||||
@@ -63,7 +75,7 @@ class OrderPricingChannelGuardTest {
|
||||
|
||||
@Test
|
||||
void downstreamHttpFailureMapsToServiceUnavailable() {
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA");
|
||||
var request = new FirstlegPricingClient.ChannelValidationRequest(20002L, 8L, "MATSON_TRUCK", "FBA", true);
|
||||
when(client.validateChannel("token", request)).thenThrow(feign(403));
|
||||
|
||||
assertUnavailable(() -> guard.requireAvailable(20002L, 8L, "MATSON_TRUCK", "FBA"));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.order.dto.OrderRequests;
|
||||
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -20,6 +21,7 @@ class PlatformOrderPageServiceTest {
|
||||
@MockBean private OrderMasterDataGuard masterDataGuard;
|
||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||
@MockBean private ProductImageGuard productImageGuard;
|
||||
@MockBean private InboundCodeModeProvider inboundCodeModeProvider;
|
||||
@Resource private OrderApplicationService service;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfig {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
@@ -23,6 +24,7 @@ class TenantOrderCountServiceTest {
|
||||
@MockBean private OrderMasterDataGuard masterDataGuard;
|
||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||
@MockBean private ProductImageGuard productImageGuard;
|
||||
@MockBean private InboundCodeModeProvider inboundCodeModeProvider;
|
||||
@Resource private OrderApplicationService service;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfig {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.order.service;
|
||||
|
||||
import com.baiyue.order.dto.TenantOrderStatisticsResponse;
|
||||
import com.baiyue.order.config.InboundCodeModeProvider;
|
||||
import com.baiyue.order.repository.OrderRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -27,6 +28,7 @@ class TenantOrderStatisticsServiceTest {
|
||||
@MockBean private OrderMasterDataGuard masterDataGuard;
|
||||
@MockBean private OrderPricingChannelGuard pricingChannelGuard;
|
||||
@MockBean private ProductImageGuard productImageGuard;
|
||||
@MockBean private InboundCodeModeProvider inboundCodeModeProvider;
|
||||
@Resource private OrderApplicationService service;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
|
||||
Reference in New Issue
Block a user