fix(firstleg): 一件代发待组托允许确认并发送费用 #34
@@ -383,11 +383,12 @@ public class FbaPricingRepository {
|
||||
.set(InboundFeeEntity::getConfirmedAt, now));
|
||||
}
|
||||
|
||||
/** 确认费用时保留物流主状态,一件代发待组托也允许确认。 */
|
||||
public int markInboundConfirmed(long tenantId, long inboundId) {
|
||||
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
|
||||
.in(InboundPricingEntity::getProcessStatus,
|
||||
List.of("FEE_CONFIRMING", "WAIT_PALLET", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"))
|
||||
List.of("FEE_CONFIRMING", "WAIT_PALLET", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"))
|
||||
.eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
|
||||
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
|
||||
}
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder;
|
||||
import com.baiyue.firstleg.mapper.InboundPricingMapper;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/** 通过真实 Mapper 执行费用状态更新,防止业务层放行但持久化条件遗漏作业状态。 */
|
||||
class FbaPricingFeeTransitionTest {
|
||||
private SqlSession session;
|
||||
private JdbcTemplate sql;
|
||||
private FbaPricingRepository repository;
|
||||
|
||||
/** 每项测试使用独立内存表,保留真实的客户和逻辑删除条件。 */
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
var ds = new JdbcDataSource();
|
||||
ds.setURL("jdbc:h2:mem:fee-transition-" + UUID.randomUUID() + ";MODE=MySQL");
|
||||
var config = new MybatisConfiguration();
|
||||
config.setEnvironment(new Environment("test", new JdbcTransactionFactory(), ds));
|
||||
config.addMapper(InboundPricingMapper.class);
|
||||
session = new MybatisSqlSessionFactoryBuilder().build(config).openSession(true);
|
||||
session.getConnection();
|
||||
sql = new JdbcTemplate(ds);
|
||||
sql.execute("CREATE TABLE fl_inbound_order(id BIGINT PRIMARY KEY,tenant_id BIGINT," +
|
||||
"process_status VARCHAR(32),fee_status VARCHAR(32),deleted BOOLEAN DEFAULT FALSE)");
|
||||
repository = new FbaPricingRepository();
|
||||
ReflectionTestUtils.setField(repository, "inbounds", session.getMapper(InboundPricingMapper.class));
|
||||
}
|
||||
|
||||
/** 关闭最后一个连接后销毁本项测试的内存数据库。 */
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
if (session != null) session.close();
|
||||
}
|
||||
|
||||
/** 费用确认和发送不得提前完成一件代发组托或回退已有履约进度。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"FEE_CONFIRMING", "WAIT_PALLET", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"})
|
||||
void confirmsAndSendsWithoutAdvancingPalletization(String processStatus) {
|
||||
insertInbound(processStatus, "CALCULATED");
|
||||
|
||||
assertThat(repository.markInboundConfirmed(10001L, 88L)).isEqualTo(1);
|
||||
assertState(processStatus, "CONFIRMED");
|
||||
assertThat(repository.markInboundConfirmed(10001L, 88L)).isZero();
|
||||
assertThat(repository.markInboundFeeSent(10001L, 88L, processStatus)).isEqualTo(1);
|
||||
assertState("FEE_CONFIRMING".equals(processStatus) ? "WAIT_PALLET" : processStatus, "SENT");
|
||||
assertThat(repository.markInboundFeeSent(10001L, 88L, processStatus)).isZero();
|
||||
}
|
||||
|
||||
/** 待组托只补费用确认资格,不能绕过费用快照的前置状态。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"UNPRICED", "CONFIRMED", "SENT", "VOIDED"})
|
||||
void rejectsConfirmationUnlessFeeIsCalculated(String feeStatus) {
|
||||
insertInbound("WAIT_PALLETIZE", feeStatus);
|
||||
assertThat(repository.markInboundConfirmed(10001L, 88L)).isZero();
|
||||
assertState("WAIT_PALLETIZE", feeStatus);
|
||||
}
|
||||
|
||||
/** 非确认阶段、其他客户和已删除入库单仍不可确认。 */
|
||||
@Test
|
||||
void preservesProcessCustomerAndDeletionGuards() {
|
||||
insertInbound("WAIT_MEASURE", "CALCULATED");
|
||||
assertThat(repository.markInboundConfirmed(10001L, 88L)).isZero();
|
||||
sql.update("UPDATE fl_inbound_order SET process_status='WAIT_PALLETIZE' WHERE id=88");
|
||||
assertThat(repository.markInboundConfirmed(20002L, 88L)).isZero();
|
||||
sql.update("UPDATE fl_inbound_order SET deleted=TRUE WHERE id=88");
|
||||
assertThat(repository.markInboundConfirmed(10001L, 88L)).isZero();
|
||||
assertState("WAIT_PALLETIZE", "CALCULATED");
|
||||
}
|
||||
|
||||
/** 模拟发送前作业状态发生变化,旧状态条件须拒绝写入。 */
|
||||
@Test
|
||||
void rejectsSendWithStaleProcessStatus() {
|
||||
insertInbound("WAIT_SPLIT", "CONFIRMED");
|
||||
assertThat(repository.markInboundFeeSent(10001L, 88L, "WAIT_PALLETIZE")).isZero();
|
||||
assertState("WAIT_SPLIT", "CONFIRMED");
|
||||
}
|
||||
|
||||
/** 准备单个客户的入库单,不调用共享环境。 */
|
||||
private void insertInbound(String processStatus, String feeStatus) {
|
||||
sql.update("INSERT INTO fl_inbound_order(id,tenant_id,process_status,fee_status) VALUES(88,10001,?,?)",
|
||||
processStatus, feeStatus);
|
||||
}
|
||||
|
||||
/** 同时核对物理作业状态与费用状态,防止用改主流程掩盖发送问题。 */
|
||||
private void assertState(String processStatus, String feeStatus) {
|
||||
assertThat(sql.queryForObject("SELECT process_status FROM fl_inbound_order WHERE id=88", String.class))
|
||||
.isEqualTo(processStatus);
|
||||
assertThat(sql.queryForObject("SELECT fee_status FROM fl_inbound_order WHERE id=88", String.class))
|
||||
.isEqualTo(feeStatus);
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -194,8 +194,9 @@ class FbaPricingRepositoryRuleListTest {
|
||||
.contains(10001L, 88L, "FEE_CONFIRMING", "CONFIRMED", "SENT", "WAIT_PALLET");
|
||||
}
|
||||
|
||||
/** 费用确认状态集覆盖待组托及后续履约阶段。 */
|
||||
@Test
|
||||
void feeConfirmationAllowsPostLoadingInboundStatuses() {
|
||||
void feeConfirmationAllowsPalletizationAndPostLoadingInboundStatuses() {
|
||||
String source;
|
||||
try {
|
||||
source = Files.readString(Path.of(
|
||||
@@ -205,7 +206,7 @@ class FbaPricingRepositoryRuleListTest {
|
||||
throw new AssertionError(exception);
|
||||
}
|
||||
assertThat(source)
|
||||
.contains("List.of(\"FEE_CONFIRMING\", \"WAIT_PALLET\", \"WAIT_SPLIT\", \"LOADING\", \"LOADED\", \"DELIVERED\")");
|
||||
.contains("List.of(\"FEE_CONFIRMING\", \"WAIT_PALLET\", \"WAIT_PALLETIZE\", \"WAIT_SPLIT\", \"LOADING\", \"LOADED\", \"DELIVERED\")");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -22,6 +22,9 @@ import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.R
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
@@ -223,11 +226,13 @@ class FbaPricingServiceTest {
|
||||
verify(financeOutbox, never()).publishInbound(any(), anyLong(), anyLong(), anyLong(), anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("待分柜边界确认费用后仍保持主流程状态")
|
||||
void confirmsAtWaitSplitWithoutRollingBackProcessStatus() {
|
||||
/** 待组托、待分柜确认费用均不推进物流主流程。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"WAIT_PALLETIZE", "WAIT_SPLIT"})
|
||||
@DisplayName("待组托或待分柜确认费用后仍保持主流程状态")
|
||||
void confirmsWithoutRollingBackProcessStatus(String processStatus) {
|
||||
InboundPricingEntity inbound = inbound("10.000", "1.0000");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
inbound.setProcessStatus(processStatus);
|
||||
inbound.setFeeStatus("CALCULATED");
|
||||
InboundFeeEntity fee = savedFee("CALCULATED");
|
||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee);
|
||||
@@ -244,7 +249,7 @@ class FbaPricingServiceTest {
|
||||
FeeSnapshotResponse response = service.confirm(
|
||||
TENANT, OPERATOR, INBOUND, "confirm-wait-split");
|
||||
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo(processStatus);
|
||||
verify(repository).markInboundConfirmed(TENANT, INBOUND);
|
||||
}
|
||||
|
||||
@@ -436,11 +441,13 @@ class FbaPricingServiceTest {
|
||||
verify(repository, never()).insertFee(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
/** 待组托发送费用保留主状态,重复请求不重复生成费用事件。 */
|
||||
@ParameterizedTest
|
||||
@CsvSource({"FEE_CONFIRMING,WAIT_PALLET", "WAIT_PALLETIZE,WAIT_PALLETIZE"})
|
||||
@DisplayName("重复发送只返回首次结果且不会重复写Outbox")
|
||||
void sendIsIdempotentAndDoesNotDuplicateOutbox() {
|
||||
void sendIsIdempotentAndDoesNotDuplicateOutbox(String processStatus, String expectedStatus) {
|
||||
InboundPricingEntity inbound = inbound("10", "1");
|
||||
inbound.setProcessStatus("FEE_CONFIRMING");
|
||||
inbound.setProcessStatus(processStatus);
|
||||
inbound.setFeeStatus("CONFIRMED");
|
||||
InboundFeeEntity confirmed = savedFee("CONFIRMED");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
@@ -448,7 +455,7 @@ class FbaPricingServiceTest {
|
||||
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_SEND", "send-once"))
|
||||
.thenReturn(null, completed("FEE_SEND", 501L));
|
||||
when(repository.sendFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1);
|
||||
when(repository.markInboundFeeSent(TENANT, INBOUND, "FEE_CONFIRMING")).thenReturn(1);
|
||||
when(repository.markInboundFeeSent(TENANT, INBOUND, processStatus)).thenReturn(1);
|
||||
when(repository.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||
when(repository.findFee(TENANT, 501L)).thenReturn(confirmed);
|
||||
InboundFeeItemEntity item = new InboundFeeItemEntity(); item.setUnitPrice(new BigDecimal("2.00"));
|
||||
@@ -459,8 +466,8 @@ class FbaPricingServiceTest {
|
||||
FeeSnapshotResponse first = service.send(TENANT, OPERATOR, INBOUND, "send-once");
|
||||
FeeSnapshotResponse repeated = service.send(TENANT, OPERATOR, INBOUND, "send-once");
|
||||
|
||||
assertThat(first.nextProcessStatus()).isEqualTo("WAIT_PALLET");
|
||||
assertThat(repeated.nextProcessStatus()).isEqualTo("WAIT_PALLET");
|
||||
assertThat(first.nextProcessStatus()).isEqualTo(expectedStatus);
|
||||
assertThat(repeated.nextProcessStatus()).isEqualTo(expectedStatus);
|
||||
verify(financeOutbox, times(1)).publishInbound(
|
||||
eq(FinanceFeeSourceOutboxService.PUBLISHED), eq(TENANT), eq(INBOUND),
|
||||
eq(501L), eq(1), eq(NOW));
|
||||
|
||||
+20
-4
@@ -36,8 +36,9 @@ class MerchantInboundBillingServiceTest {
|
||||
@MockBean private InboundFeeDetailService feeDetails;
|
||||
@Resource private MerchantInboundBillingService service;
|
||||
|
||||
/** 测量审核通过并发送费用后,待组托与后续履约阶段均可查看客户费用。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"})
|
||||
@ValueSource(strings = {"WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"})
|
||||
void shouldKeepPublishedBillingVisibleAfterFulfillmentProgresses(String processStatus) {
|
||||
InboundOrderEntity inbound = inbound(processStatus, "APPROVED", "SENT");
|
||||
InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class),
|
||||
@@ -84,9 +85,11 @@ class MerchantInboundBillingServiceTest {
|
||||
verify(orders).findByOrderId(10001L, 77L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleCase02() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "PENDING", "SENT");
|
||||
/** 待组托不绕过客户费用可见性的测量审核条件。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"WAIT_PALLETIZE", "WAIT_SPLIT"})
|
||||
void shouldHandleCase02(String processStatus) {
|
||||
InboundOrderEntity inbound = inbound(processStatus, "PENDING", "SENT");
|
||||
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
|
||||
|
||||
assertThatThrownBy(() -> service.find(10001L, 77L))
|
||||
@@ -105,6 +108,19 @@ class MerchantInboundBillingServiceTest {
|
||||
verify(pricing, never()).latest(20002L, 88L);
|
||||
}
|
||||
|
||||
/** 客户可在组托前确认已审核并发送的费用,继续使用当前费用版本和幂等键。 */
|
||||
@Test
|
||||
void confirmsPublishedFeeWhileWaitingForPalletization() {
|
||||
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound("WAIT_PALLETIZE", "APPROVED", "SENT"));
|
||||
FeeSnapshotResponse fee = mock(FeeSnapshotResponse.class);
|
||||
when(pricing.confirmByCustomer(10001L, 7L, "客户成员", 88L, 3, "confirm-palletize"))
|
||||
.thenReturn(fee);
|
||||
|
||||
assertThat(service.confirmFee(10001L, 7L, "客户成员", 77L, 3, "confirm-palletize"))
|
||||
.isSameAs(fee);
|
||||
verify(pricing).confirmByCustomer(10001L, 7L, "客户成员", 88L, 3, "confirm-palletize");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleCase04() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "SENT");
|
||||
|
||||
Reference in New Issue
Block a user