From 50687c851df57feec07cb6e606bc1b22bd0b9a0c Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 26 Oct 2026 11:38:51 +0800 Subject: [PATCH 1/2] fix(firstleg): allow fee confirmation while awaiting palletization --- .../repository/FbaPricingRepository.java | 3 +- .../FbaPricingFeeTransitionTest.java | 107 ++++++++++++++++++ .../service/FbaPricingServiceTest.java | 29 +++-- .../MerchantInboundBillingServiceTest.java | 24 +++- 4 files changed, 147 insertions(+), 16 deletions(-) create mode 100644 firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingFeeTransitionTest.java diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java index 96ee91ee7..4949dee6f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/FbaPricingRepository.java @@ -383,11 +383,12 @@ public class FbaPricingRepository { .set(InboundFeeEntity::getConfirmedAt, now)); } + /** 确认费用时保留物流主状态,一件代发待组托也允许确认。 */ public int markInboundConfirmed(long tenantId, long inboundId) { return inbounds.update(null, new LambdaUpdateWrapper() .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")); } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingFeeTransitionTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingFeeTransitionTest.java new file mode 100644 index 000000000..3a9f0ff00 --- /dev/null +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingFeeTransitionTest.java @@ -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); + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java index 887652f7d..170e617d6 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java @@ -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)); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java index 11bbbc6b8..1ee1091db 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MerchantInboundBillingServiceTest.java @@ -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"); -- 2.54.0 From 0fe1d1056661d591f9739964450f35421b076807 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 26 Oct 2026 11:42:42 +0800 Subject: [PATCH 2/2] test(firstleg): include palletization in fee confirmation contract --- .../repository/FbaPricingRepositoryRuleListTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java index 2372ee74f..5d2238572 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/FbaPricingRepositoryRuleListTest.java @@ -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 -- 2.54.0