Merge branch 'test' into test-02huogui
# Conflicts: # deploy/sql/头程数据库统一增量-MySQL8.sql # firstleg/src/main/java/com/baiyue/firstleg/BaiyueFirstlegApplication.java # firstleg/src/main/resources/openapi/firstleg-v1.yaml
This commit is contained in:
@@ -13,24 +13,30 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@DisplayName("FBA计费OpenAPI契约")
|
||||
class FbaPricingContractTest {
|
||||
@Test
|
||||
@DisplayName("只有基础资料列表允许省略企业参数")
|
||||
void onlyMasterDataListsAllowOptionalTenant() {
|
||||
for (String listMethod : new String[] {"rules", "rates"}) {
|
||||
Method method = method(listMethod);
|
||||
assertThat(method.getParameterTypes()[1]).isEqualTo(Long.class);
|
||||
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
|
||||
}
|
||||
@DisplayName("客户规则保留客户参数,船司价格使用平台公共作用域")
|
||||
void keepsCustomerRuleScopeAndRemovesCarrierRateTenantScope() {
|
||||
Method rules = method("rules");
|
||||
assertThat(rules.getParameterTypes()[1]).isEqualTo(Long.class);
|
||||
assertThat(rules.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
|
||||
|
||||
for (String scopedMethod : new String[] {"rule", "createRule", "retireRule",
|
||||
"rate", "createRate", "retireRate"}) {
|
||||
Method rates = method("rates");
|
||||
assertThat(rates.getParameterTypes()).doesNotContain(Long.class);
|
||||
|
||||
for (String scopedMethod : new String[] {"rule", "createRule", "retireRule"}) {
|
||||
Method method = method(scopedMethod);
|
||||
assertThat(method.getParameterTypes()[1]).isEqualTo(long.class);
|
||||
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
|
||||
}
|
||||
|
||||
for (String publicMethod : new String[] {"rate", "createRate", "retireRate"}) {
|
||||
assertThat(java.util.Arrays.stream(method(publicMethod).getParameters())
|
||||
.map(parameter -> parameter.getAnnotation(RequestParam.class)))
|
||||
.allMatch(annotation -> annotation == null);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("契约声明自动发布、已发布费用调整和幂等头")
|
||||
@DisplayName("契约声明二期边界计费、已发布费用调整和幂等头")
|
||||
void documentsPricingLifecycle() throws Exception {
|
||||
String openapi = new ClassPathResource("openapi/fba-pricing-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
@@ -42,7 +48,8 @@ class FbaPricingContractTest {
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/fees/send:")
|
||||
.contains("/api/platform/v1/inbounds/{inboundId}/fees/published:")
|
||||
.contains("Idempotency-Key")
|
||||
.contains("客服测量审核通过后自动计费、发布并完成入库")
|
||||
.contains("主流程已到 WAIT_SPLIT,也可独立试算且不回退主流程状态")
|
||||
.contains("该动作涉及后续仓储流程,本轮二期验收不调用")
|
||||
.contains("不回退 INBOUNDED 状态")
|
||||
.doesNotContain("应收")
|
||||
.doesNotContain("应付")
|
||||
@@ -62,7 +69,7 @@ class FbaPricingContractTest {
|
||||
openapi.indexOf("/api/platform/v1/fba-pricing/carrier-rates:"),
|
||||
openapi.indexOf("/api/platform/v1/fba-pricing/carrier-rates/versions:"));
|
||||
assertThat(carrierRates)
|
||||
.contains("OptionalTenantId")
|
||||
.doesNotContain("OptionalTenantId")
|
||||
.contains("name: rateCode");
|
||||
assertThat(openapi)
|
||||
.contains("OptionalTenantId: {name: tenantId, in: query, required: false")
|
||||
|
||||
+25
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.InboundOrderReference;
|
||||
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
|
||||
import com.baiyue.firstleg.service.InboundOrderCreationService;
|
||||
import com.baiyue.firstleg.service.UnifiedInboundQueryService;
|
||||
import com.baiyue.firstleg.dto.UnifiedInboundQuery;
|
||||
@@ -63,4 +64,28 @@ class InternalInboundOrderReferenceControllerTest {
|
||||
.isInstanceOf(BizException.class);
|
||||
verify(unifiedQueryService).page(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedOrderRequiresInboundCode() {
|
||||
assertThatThrownBy(() -> controller.createFromApprovedOrder(
|
||||
"internal-token", approvedCommand(" ", null)))
|
||||
.isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvedOrderAllowsMissingCarrier() {
|
||||
OrderApprovedInboundCommand command = approvedCommand("RK-77", null);
|
||||
when(service.createFromApprovedOrder(command)).thenReturn(88L);
|
||||
|
||||
assertThat(controller.createFromApprovedOrder("internal-token", command).data()).isEqualTo(88L);
|
||||
verify(service).createFromApprovedOrder(command);
|
||||
}
|
||||
|
||||
private static OrderApprovedInboundCommand approvedCommand(String inboundCode, Long carrierId) {
|
||||
return new OrderApprovedInboundCommand(
|
||||
10001L, 77L, "FL-77", inboundCode, 7L, "测试客户", 9L, "义乌仓",
|
||||
null, null, null, "FBA", "AIR", "空运", carrierId,
|
||||
carrierId == null ? null : "承运商", null, null, null, null,
|
||||
null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
+48
-7
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
|
||||
import com.baiyue.firstleg.entity.CarrierFbaRateEntity;
|
||||
import com.baiyue.firstleg.entity.InboundPricingIdempotencyEntity;
|
||||
import com.baiyue.firstleg.entity.InboundPricingEntity;
|
||||
import com.baiyue.firstleg.mapper.CarrierFbaRateMapper;
|
||||
import com.baiyue.firstleg.mapper.CustomerChargeRuleMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundFeeItemMapper;
|
||||
@@ -48,6 +49,8 @@ class FbaPricingRepositoryRuleListTest {
|
||||
CustomerChargeRuleEntity.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"),
|
||||
CarrierFbaRateEntity.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"),
|
||||
InboundPricingEntity.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -61,14 +64,52 @@ class FbaPricingRepositoryRuleListTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformRateListOmitsTenantConditionWhenTenantIsAbsent() {
|
||||
assertThat(rateQueryValues(null, null)).isEmpty();
|
||||
void platformRateListAlwaysUsesPublicScope() {
|
||||
assertThat(rateQueryValues(null)).containsExactly(CarrierFbaRateEntity.PLATFORM_SCOPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformRateListKeepsTenantAndRateCodeFiltersWhenProvided() {
|
||||
assertThat(rateQueryValues(10001L, "RATE-1"))
|
||||
.containsExactlyInAnyOrder(10001L, "RATE-1");
|
||||
void platformRateListKeepsPublicScopeAndRateCodeFiltersWhenProvided() {
|
||||
assertThat(rateQueryValues("RATE-1"))
|
||||
.containsExactlyInAnyOrder(CarrierFbaRateEntity.PLATFORM_SCOPE, "RATE-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformRateCreateQueryLocksPublicScope() {
|
||||
when(rates.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||
|
||||
repository.listRatesForUpdate(null);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaQueryWrapper<CarrierFbaRateEntity>> wrapper =
|
||||
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(rates).selectList(wrapper.capture());
|
||||
assertThat(wrapper.getValue().getCustomSqlSegment()).contains("FOR UPDATE");
|
||||
assertThat(wrapper.getValue().getParamNameValuePairs().values())
|
||||
.containsExactly(CarrierFbaRateEntity.PLATFORM_SCOPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void carrierRateMatchingAlwaysUsesPlatformScope() {
|
||||
InboundPricingEntity inbound = new InboundPricingEntity();
|
||||
inbound.setTenantId(10001L);
|
||||
inbound.setWarehouseId(21L);
|
||||
inbound.setBusinessType("FBA_SEA");
|
||||
inbound.setChannelCode("SEA-TAX");
|
||||
inbound.setCarrierId(31L);
|
||||
inbound.setDestinationCode("ONT8");
|
||||
when(rates.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||
|
||||
repository.matchRates(inbound, "KG", java.time.LocalDateTime.of(2026, 8, 1, 0, 0));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaQueryWrapper<CarrierFbaRateEntity>> wrapper =
|
||||
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||
verify(rates).selectList(wrapper.capture());
|
||||
wrapper.getValue().getSqlSegment();
|
||||
assertThat(wrapper.getValue().getParamNameValuePairs().values())
|
||||
.contains(CarrierFbaRateEntity.PLATFORM_SCOPE)
|
||||
.doesNotContain(10001L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -122,10 +163,10 @@ class FbaPricingRepositoryRuleListTest {
|
||||
return wrapper.getValue().getParamNameValuePairs().values();
|
||||
}
|
||||
|
||||
private Collection<Object> rateQueryValues(Long tenantId, String rateCode) {
|
||||
private Collection<Object> rateQueryValues(String rateCode) {
|
||||
when(rates.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||
|
||||
repository.listRates(tenantId, rateCode);
|
||||
repository.listRates(rateCode);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaQueryWrapper<CarrierFbaRateEntity>> wrapper =
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.CustomerRuleVersionRequest;
|
||||
import com.baiyue.firstleg.dto.FbaPricingDtos.CarrierRateVersionRequest;
|
||||
import com.baiyue.firstleg.entity.CarrierFbaRateEntity;
|
||||
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
|
||||
import com.baiyue.firstleg.repository.FbaPricingRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -73,11 +75,33 @@ class FbaMasterDataServiceTest {
|
||||
.hasMessageContaining("有效期重叠");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("船司价格版本固定写入平台公共作用域")
|
||||
void createsCarrierRateInPlatformScope() {
|
||||
when(repository.listRatesForUpdate(null)).thenReturn(List.of());
|
||||
when(repository.nextRateVersion("RATE-1")).thenReturn(1);
|
||||
doAnswer(invocation -> { ((CarrierFbaRateEntity) invocation.getArgument(0)).setId(20L); return null; })
|
||||
.when(repository).insertRate(any());
|
||||
|
||||
CarrierFbaRateEntity created = service.createRateVersion(2L, carrierRateRequest());
|
||||
|
||||
assertThat(created.getTenantId()).isEqualTo(CarrierFbaRateEntity.PLATFORM_SCOPE);
|
||||
verify(repository).listRatesForUpdate(null);
|
||||
verify(repository).nextRateVersion("RATE-1");
|
||||
verify(repository).insertRate(any());
|
||||
}
|
||||
|
||||
private CustomerRuleVersionRequest request(LocalDateTime from, LocalDateTime to) {
|
||||
return new CustomerRuleVersionRequest("CR-1", 100L, "客户A", "FBA_SEA", "KG",
|
||||
"TAX_INCLUDED", new BigDecimal("167"), 3, "CEILING", "USD", from, to, null);
|
||||
}
|
||||
|
||||
private CarrierRateVersionRequest carrierRateRequest() {
|
||||
return new CarrierRateVersionRequest("RATE-1", 21L, "义乌仓", "FBA_SEA", "SEA-TAX",
|
||||
31L, "船司A", "ONT8", "KG", new BigDecimal("2.50"), "USD", new BigDecimal("10"),
|
||||
AUGUST, null, null);
|
||||
}
|
||||
|
||||
private CustomerChargeRuleEntity activeRule(LocalDateTime from, LocalDateTime to) {
|
||||
CustomerChargeRuleEntity value = new CustomerChargeRuleEntity();
|
||||
value.setRuleCode("CR-1"); value.setRuleVersion(1); value.setCustomerId(100L);
|
||||
|
||||
@@ -100,6 +100,51 @@ class FbaPricingServiceTest {
|
||||
assertThat(response.totalAmount()).isEqualByComparingTo("100.01");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待分柜边界可独立计费且不回退主流程状态")
|
||||
void calculatesAtWaitSplitWithoutRollingBackProcessStatus() {
|
||||
readyRepository();
|
||||
InboundPricingEntity inbound = inbound("100.000", "1.0000");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
CustomerChargeRuleEntity rule = rule("KG", "167.000000", 3, "CEILING");
|
||||
CarrierFbaRateEntity rate = rate("KG", "2.000000", "0.00");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(repository.matchRules(inbound, NOW)).thenReturn(List.of(rule));
|
||||
when(repository.matchRates(inbound, "KG", NOW)).thenReturn(List.of(rate));
|
||||
|
||||
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "wait-split");
|
||||
|
||||
assertThat(response.feeStatus()).isEqualTo("CALCULATED");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository).markInboundCalculated(TENANT, INBOUND, "WAIT_SPLIT", "KG",
|
||||
new BigDecimal("167.000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待分柜边界确认费用后仍保持主流程状态")
|
||||
void confirmsAtWaitSplitWithoutRollingBackProcessStatus() {
|
||||
InboundPricingEntity inbound = inbound("10.000", "1.0000");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
inbound.setFeeStatus("CALCULATED");
|
||||
InboundFeeEntity fee = savedFee("CALCULATED");
|
||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee);
|
||||
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CONFIRM", "confirm-wait-split"))
|
||||
.thenReturn(null);
|
||||
doAnswer(invocation -> { ((InboundPricingIdempotencyEntity) invocation.getArgument(0)).setId(900L); return null; })
|
||||
.when(repository).insertIdempotency(any());
|
||||
when(repository.confirmFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1);
|
||||
when(repository.markInboundConfirmed(TENANT, INBOUND)).thenReturn(1);
|
||||
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||
when(repository.findFee(TENANT, 501L)).thenReturn(fee);
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
|
||||
FeeSnapshotResponse response = service.confirm(
|
||||
TENANT, OPERATOR, INBOUND, "confirm-wait-split");
|
||||
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository).markInboundConfirmed(TENANT, INBOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("审核未通过的测量快照禁止计费")
|
||||
void rejectsUnapprovedMeasurementSnapshot() {
|
||||
@@ -137,15 +182,18 @@ class FbaPricingServiceTest {
|
||||
@DisplayName("同一幂等键重复试算直接返回原费用快照")
|
||||
void returnsOriginalSnapshotForRepeatedCalculation() {
|
||||
InboundPricingIdempotencyEntity completed = completed("FEE_CALCULATE", 501L);
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_CALCULATE", "same")).thenReturn(completed);
|
||||
when(repository.findFee(TENANT, 501L)).thenReturn(savedFee("CALCULATED"));
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound("10", "1"));
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
InboundFeeItemEntity item = new InboundFeeItemEntity(); item.setUnitPrice(new BigDecimal("2.00"));
|
||||
when(repository.findBaseItem(TENANT, 501L)).thenReturn(item);
|
||||
|
||||
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "same");
|
||||
|
||||
assertThat(response.feeId()).isEqualTo(501L);
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository, never()).matchRules(any(), any());
|
||||
verify(repository, never()).insertFee(any());
|
||||
}
|
||||
@@ -233,7 +281,7 @@ class FbaPricingServiceTest {
|
||||
doAnswer(invocation -> { ((InboundFeeEntity) invocation.getArgument(0)).setId(501L); return null; })
|
||||
.when(repository).insertFee(any());
|
||||
when(repository.nextFeeVersion(TENANT, INBOUND)).thenReturn(1);
|
||||
when(repository.markInboundCalculated(eq(TENANT), eq(INBOUND), any(), any())).thenReturn(1);
|
||||
when(repository.markInboundCalculated(eq(TENANT), eq(INBOUND), any(), any(), any())).thenReturn(1);
|
||||
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||
}
|
||||
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundOrderCreationServiceTest {
|
||||
@Mock
|
||||
private InboundOrderMapper orders;
|
||||
@Mock
|
||||
private InboundStatusHistoryMapper histories;
|
||||
@InjectMocks
|
||||
private InboundOrderCreationService service;
|
||||
|
||||
@Test
|
||||
void shouldUseSubmittedInboundCodeAndKeepReplayIdempotentWithoutCarrier() {
|
||||
OrderApprovedInboundCommand command = approvedCommand("RK-CUSTOM-77");
|
||||
InboundOrderEntity existing = new InboundOrderEntity();
|
||||
existing.setId(900L);
|
||||
existing.setTenantId(10001L);
|
||||
existing.setOrderId(77L);
|
||||
existing.setInboundCode("RK-CUSTOM-77");
|
||||
when(orders.selectOne(any(Wrapper.class))).thenReturn(null, existing);
|
||||
|
||||
long createdId = service.createFromApprovedOrder(command);
|
||||
long replayedId = service.createFromApprovedOrder(command);
|
||||
|
||||
ArgumentCaptor<InboundOrderEntity> inboundCaptor = ArgumentCaptor.forClass(InboundOrderEntity.class);
|
||||
verify(orders, times(1)).insert(inboundCaptor.capture());
|
||||
verify(histories, times(1)).insert(any(InboundStatusHistoryEntity.class));
|
||||
assertThat(inboundCaptor.getValue().getInboundCode()).isEqualTo("RK-CUSTOM-77");
|
||||
assertThat(inboundCaptor.getValue().getCarrierId()).isNull();
|
||||
assertThat(createdId).isEqualTo(inboundCaptor.getValue().getId());
|
||||
assertThat(replayedId).isEqualTo(900L);
|
||||
}
|
||||
|
||||
private static OrderApprovedInboundCommand approvedCommand(String inboundCode) {
|
||||
return new OrderApprovedInboundCommand(
|
||||
10001L, 77L, "FL-77", inboundCode, 7L, "测试客户", 9L, "义乌仓",
|
||||
null, null, null, "FBA", "AIR", "空运", null, null,
|
||||
null, null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user