Compare commits
21
Commits
dev
...
ba0bf9ebb7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba0bf9ebb7 | ||
|
|
8d211acf5b | ||
|
|
5e9cebcf85 | ||
|
|
0fe1d10566 | ||
|
|
50687c851d | ||
|
|
2ae8e7ce94 | ||
|
|
e31dcf80f9 | ||
|
|
2573bd92cb | ||
|
|
20b2a7e058 | ||
|
|
c0cd2c2183 | ||
|
|
46d359241f | ||
|
|
57fcf295b1 | ||
|
|
5950806924 | ||
|
|
4dc3d1752e | ||
|
|
f60f44793f | ||
|
|
d4eb0ef023 | ||
|
|
29bbd85ed6 | ||
|
|
f010dcc4e0 | ||
|
|
7151222836 | ||
|
|
4825f782a3 | ||
|
|
a7ca6b98f4 |
@@ -383,11 +383,12 @@ public class FbaPricingRepository {
|
|||||||
.set(InboundFeeEntity::getConfirmedAt, now));
|
.set(InboundFeeEntity::getConfirmedAt, now));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 确认费用时保留物流主状态,一件代发待组托也允许确认。 */
|
||||||
public int markInboundConfirmed(long tenantId, long inboundId) {
|
public int markInboundConfirmed(long tenantId, long inboundId) {
|
||||||
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
|
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||||
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
|
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
|
||||||
.in(InboundPricingEntity::getProcessStatus,
|
.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")
|
.eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
|
||||||
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
|
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -9,11 +9,12 @@ import java.util.List;
|
|||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface ContainerCalendarSourceMapper {
|
public interface ContainerCalendarSourceMapper {
|
||||||
/** 按明确来源柜读取与预约日历相同的到港事实,实际到港优先。 */
|
/** 按明确来源柜读取到港事实;目的港优先显示名称,未填名称时显示已保存代码。 */
|
||||||
@Select("""
|
@Select("""
|
||||||
<script>
|
<script>
|
||||||
SELECT c.id containerBookingId,c.cabinet_no cabinetNo,c.container_no containerNo,
|
SELECT c.id containerBookingId,c.cabinet_no cabinetNo,c.container_no containerNo,
|
||||||
c.vessel_name vesselName,c.voyage_no voyageNo,c.destination_port_name destinationPortName,
|
c.vessel_name vesselName,c.voyage_no voyageNo,
|
||||||
|
COALESCE(NULLIF(TRIM(c.destination_port_name),''),NULLIF(TRIM(c.destination_port_code),'')) destinationPortName,
|
||||||
c.destination_warehouse_code destinationWarehouseCode,
|
c.destination_warehouse_code destinationWarehouseCode,
|
||||||
COALESCE(n.actual_at,c.actual_port_arrival_at,c.estimated_port_arrival_at) arrivalAt,
|
COALESCE(n.actual_at,c.actual_port_arrival_at,c.estimated_port_arrival_at) arrivalAt,
|
||||||
CASE WHEN COALESCE(n.actual_at,c.actual_port_arrival_at) IS NOT NULL THEN 'ACTUAL' ELSE 'ESTIMATED' END arrivalType
|
CASE WHEN COALESCE(n.actual_at,c.actual_port_arrival_at) IS NOT NULL THEN 'ACTUAL' ELSE 'ESTIMATED' END arrivalType
|
||||||
@@ -28,10 +29,11 @@ public interface ContainerCalendarSourceMapper {
|
|||||||
""")
|
""")
|
||||||
List<ContainerCalendarSource> byIds(@Param("ids") List<Long> ids);
|
List<ContainerCalendarSource> byIds(@Param("ids") List<Long> ids);
|
||||||
|
|
||||||
/** 同时选出到港、缺日期和可能还柜的有效出仓柜,最终日期由业务服务复核。 */
|
/** 选出到港、缺日期和可能还柜的有效出仓柜;目的港名称为空时展示代码,最终日期由服务复核。 */
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT c.id containerBookingId,c.cabinet_no cabinetNo,c.container_no containerNo,
|
SELECT c.id containerBookingId,c.cabinet_no cabinetNo,c.container_no containerNo,
|
||||||
c.vessel_name vesselName,c.voyage_no voyageNo,c.destination_port_name destinationPortName,
|
c.vessel_name vesselName,c.voyage_no voyageNo,
|
||||||
|
COALESCE(NULLIF(TRIM(c.destination_port_name),''),NULLIF(TRIM(c.destination_port_code),'')) destinationPortName,
|
||||||
c.destination_warehouse_code destinationWarehouseCode,
|
c.destination_warehouse_code destinationWarehouseCode,
|
||||||
COALESCE(n.actual_at,c.actual_port_arrival_at,c.estimated_port_arrival_at) arrivalAt,
|
COALESCE(n.actual_at,c.actual_port_arrival_at,c.estimated_port_arrival_at) arrivalAt,
|
||||||
CASE WHEN COALESCE(n.actual_at,c.actual_port_arrival_at) IS NOT NULL THEN 'ACTUAL' ELSE 'ESTIMATED' END arrivalType
|
CASE WHEN COALESCE(n.actual_at,c.actual_port_arrival_at) IS NOT NULL THEN 'ACTUAL' ELSE 'ESTIMATED' END arrivalType
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ paths:
|
|||||||
containerNo: {type: string, nullable: true}
|
containerNo: {type: string, nullable: true}
|
||||||
vesselName: {type: string, nullable: true}
|
vesselName: {type: string, nullable: true}
|
||||||
voyageNo: {type: string, nullable: true}
|
voyageNo: {type: string, nullable: true}
|
||||||
destinationPortName: {type: string, nullable: true}
|
destinationPortName: {type: string, nullable: true, description: 目的港展示值,优先名称,名称为空时使用已保存的港口代码,两者均为空时返回null}
|
||||||
destinationWarehouseCode: {type: string, nullable: true}
|
destinationWarehouseCode: {type: string, nullable: true}
|
||||||
arrivalAt: {type: string, format: date-time, nullable: true}
|
arrivalAt: {type: string, format: date-time, nullable: true}
|
||||||
arrivalType: {type: string, enum: [ACTUAL, ESTIMATED]}
|
arrivalType: {type: string, enum: [ACTUAL, ESTIMATED]}
|
||||||
|
|||||||
+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");
|
.contains(10001L, 88L, "FEE_CONFIRMING", "CONFIRMED", "SENT", "WAIT_PALLET");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 费用确认状态集覆盖待组托及后续履约阶段。 */
|
||||||
@Test
|
@Test
|
||||||
void feeConfirmationAllowsPostLoadingInboundStatuses() {
|
void feeConfirmationAllowsPalletizationAndPostLoadingInboundStatuses() {
|
||||||
String source;
|
String source;
|
||||||
try {
|
try {
|
||||||
source = Files.readString(Path.of(
|
source = Files.readString(Path.of(
|
||||||
@@ -205,7 +206,7 @@ class FbaPricingRepositoryRuleListTest {
|
|||||||
throw new AssertionError(exception);
|
throw new AssertionError(exception);
|
||||||
}
|
}
|
||||||
assertThat(source)
|
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
|
@Test
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.R
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
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.mockito.ArgumentCaptor;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
@@ -223,11 +226,13 @@ class FbaPricingServiceTest {
|
|||||||
verify(financeOutbox, never()).publishInbound(any(), anyLong(), anyLong(), anyLong(), anyInt(), any());
|
verify(financeOutbox, never()).publishInbound(any(), anyLong(), anyLong(), anyLong(), anyInt(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
/** 待组托、待分柜确认费用均不推进物流主流程。 */
|
||||||
@DisplayName("待分柜边界确认费用后仍保持主流程状态")
|
@ParameterizedTest
|
||||||
void confirmsAtWaitSplitWithoutRollingBackProcessStatus() {
|
@ValueSource(strings = {"WAIT_PALLETIZE", "WAIT_SPLIT"})
|
||||||
|
@DisplayName("待组托或待分柜确认费用后仍保持主流程状态")
|
||||||
|
void confirmsWithoutRollingBackProcessStatus(String processStatus) {
|
||||||
InboundPricingEntity inbound = inbound("10.000", "1.0000");
|
InboundPricingEntity inbound = inbound("10.000", "1.0000");
|
||||||
inbound.setProcessStatus("WAIT_SPLIT");
|
inbound.setProcessStatus(processStatus);
|
||||||
inbound.setFeeStatus("CALCULATED");
|
inbound.setFeeStatus("CALCULATED");
|
||||||
InboundFeeEntity fee = savedFee("CALCULATED");
|
InboundFeeEntity fee = savedFee("CALCULATED");
|
||||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee);
|
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee);
|
||||||
@@ -244,7 +249,7 @@ class FbaPricingServiceTest {
|
|||||||
FeeSnapshotResponse response = service.confirm(
|
FeeSnapshotResponse response = service.confirm(
|
||||||
TENANT, OPERATOR, INBOUND, "confirm-wait-split");
|
TENANT, OPERATOR, INBOUND, "confirm-wait-split");
|
||||||
|
|
||||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
assertThat(response.nextProcessStatus()).isEqualTo(processStatus);
|
||||||
verify(repository).markInboundConfirmed(TENANT, INBOUND);
|
verify(repository).markInboundConfirmed(TENANT, INBOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,11 +441,13 @@ class FbaPricingServiceTest {
|
|||||||
verify(repository, never()).insertFee(any());
|
verify(repository, never()).insertFee(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
/** 待组托发送费用保留主状态,重复请求不重复生成费用事件。 */
|
||||||
|
@ParameterizedTest
|
||||||
|
@CsvSource({"FEE_CONFIRMING,WAIT_PALLET", "WAIT_PALLETIZE,WAIT_PALLETIZE"})
|
||||||
@DisplayName("重复发送只返回首次结果且不会重复写Outbox")
|
@DisplayName("重复发送只返回首次结果且不会重复写Outbox")
|
||||||
void sendIsIdempotentAndDoesNotDuplicateOutbox() {
|
void sendIsIdempotentAndDoesNotDuplicateOutbox(String processStatus, String expectedStatus) {
|
||||||
InboundPricingEntity inbound = inbound("10", "1");
|
InboundPricingEntity inbound = inbound("10", "1");
|
||||||
inbound.setProcessStatus("FEE_CONFIRMING");
|
inbound.setProcessStatus(processStatus);
|
||||||
inbound.setFeeStatus("CONFIRMED");
|
inbound.setFeeStatus("CONFIRMED");
|
||||||
InboundFeeEntity confirmed = savedFee("CONFIRMED");
|
InboundFeeEntity confirmed = savedFee("CONFIRMED");
|
||||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||||
@@ -448,7 +455,7 @@ class FbaPricingServiceTest {
|
|||||||
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_SEND", "send-once"))
|
when(repository.findIdempotency(TENANT, OPERATOR, "FEE_SEND", "send-once"))
|
||||||
.thenReturn(null, completed("FEE_SEND", 501L));
|
.thenReturn(null, completed("FEE_SEND", 501L));
|
||||||
when(repository.sendFee(TENANT, 501L, OPERATOR, NOW)).thenReturn(1);
|
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.completeIdempotency(any(), eq(501L), eq(NOW))).thenReturn(1);
|
||||||
when(repository.findFee(TENANT, 501L)).thenReturn(confirmed);
|
when(repository.findFee(TENANT, 501L)).thenReturn(confirmed);
|
||||||
InboundFeeItemEntity item = new InboundFeeItemEntity(); item.setUnitPrice(new BigDecimal("2.00"));
|
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 first = service.send(TENANT, OPERATOR, INBOUND, "send-once");
|
||||||
FeeSnapshotResponse repeated = service.send(TENANT, OPERATOR, INBOUND, "send-once");
|
FeeSnapshotResponse repeated = service.send(TENANT, OPERATOR, INBOUND, "send-once");
|
||||||
|
|
||||||
assertThat(first.nextProcessStatus()).isEqualTo("WAIT_PALLET");
|
assertThat(first.nextProcessStatus()).isEqualTo(expectedStatus);
|
||||||
assertThat(repeated.nextProcessStatus()).isEqualTo("WAIT_PALLET");
|
assertThat(repeated.nextProcessStatus()).isEqualTo(expectedStatus);
|
||||||
verify(financeOutbox, times(1)).publishInbound(
|
verify(financeOutbox, times(1)).publishInbound(
|
||||||
eq(FinanceFeeSourceOutboxService.PUBLISHED), eq(TENANT), eq(INBOUND),
|
eq(FinanceFeeSourceOutboxService.PUBLISHED), eq(TENANT), eq(INBOUND),
|
||||||
eq(501L), eq(1), eq(NOW));
|
eq(501L), eq(1), eq(NOW));
|
||||||
|
|||||||
+20
-4
@@ -36,8 +36,9 @@ class MerchantInboundBillingServiceTest {
|
|||||||
@MockBean private InboundFeeDetailService feeDetails;
|
@MockBean private InboundFeeDetailService feeDetails;
|
||||||
@Resource private MerchantInboundBillingService service;
|
@Resource private MerchantInboundBillingService service;
|
||||||
|
|
||||||
|
/** 测量审核通过并发送费用后,待组托与后续履约阶段均可查看客户费用。 */
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = {"WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"})
|
@ValueSource(strings = {"WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"})
|
||||||
void shouldKeepPublishedBillingVisibleAfterFulfillmentProgresses(String processStatus) {
|
void shouldKeepPublishedBillingVisibleAfterFulfillmentProgresses(String processStatus) {
|
||||||
InboundOrderEntity inbound = inbound(processStatus, "APPROVED", "SENT");
|
InboundOrderEntity inbound = inbound(processStatus, "APPROVED", "SENT");
|
||||||
InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class),
|
InboundOrderDetail detail = new InboundOrderDetail(mock(com.baiyue.firstleg.dto.InboundOrderSummary.class),
|
||||||
@@ -84,9 +85,11 @@ class MerchantInboundBillingServiceTest {
|
|||||||
verify(orders).findByOrderId(10001L, 77L);
|
verify(orders).findByOrderId(10001L, 77L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
/** 待组托不绕过客户费用可见性的测量审核条件。 */
|
||||||
void shouldHandleCase02() {
|
@ParameterizedTest
|
||||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "PENDING", "SENT");
|
@ValueSource(strings = {"WAIT_PALLETIZE", "WAIT_SPLIT"})
|
||||||
|
void shouldHandleCase02(String processStatus) {
|
||||||
|
InboundOrderEntity inbound = inbound(processStatus, "PENDING", "SENT");
|
||||||
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
|
when(orders.findByOrderId(10001L, 77L)).thenReturn(inbound);
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.find(10001L, 77L))
|
assertThatThrownBy(() -> service.find(10001L, 77L))
|
||||||
@@ -105,6 +108,19 @@ class MerchantInboundBillingServiceTest {
|
|||||||
verify(pricing, never()).latest(20002L, 88L);
|
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
|
@Test
|
||||||
void shouldHandleCase04() {
|
void shouldHandleCase04() {
|
||||||
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "SENT");
|
InboundOrderEntity inbound = inbound("WAIT_SPLIT", "APPROVED", "SENT");
|
||||||
|
|||||||
+37
-2
@@ -11,6 +11,41 @@ import java.time.LocalDateTime;
|
|||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
class ContainerCalendarSourceMapperTest {
|
class ContainerCalendarSourceMapperTest {
|
||||||
|
/** 两个日历来源入口均优先展示港口名称,仅填代码时仍可回显,缺失时保持空值。 */
|
||||||
|
@Test
|
||||||
|
void displaysDestinationNameOrSavedCodeAcrossBothEntrypoints() throws Exception {
|
||||||
|
var ds = new JdbcDataSource();
|
||||||
|
ds.setURL("jdbc:h2:mem:calendar-port-display;MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||||
|
try (var connection = ds.getConnection(); var sql = connection.createStatement()) {
|
||||||
|
sql.execute("CREATE TABLE fl_container_booking(id BIGINT,cabinet_no VARCHAR,container_no VARCHAR," +
|
||||||
|
"vessel_name VARCHAR,voyage_no VARCHAR,destination_port_name VARCHAR,destination_port_code VARCHAR,destination_warehouse_code VARCHAR," +
|
||||||
|
"actual_port_arrival_at TIMESTAMP,estimated_port_arrival_at TIMESTAMP,unloading_completed_at TIMESTAMP,record_status VARCHAR)");
|
||||||
|
sql.execute("CREATE TABLE fl_terminal_drayage(container_booking_id BIGINT,actual_empty_return_at TIMESTAMP)");
|
||||||
|
sql.execute("CREATE TABLE fl_carrier_schedule_node_fact(id BIGINT,container_booking_id BIGINT,standard_node VARCHAR,actual_at TIMESTAMP,observed_at TIMESTAMP)");
|
||||||
|
sql.execute("CREATE TABLE fl_direct_delivery_unloading(container_booking_id BIGINT,actual_at TIMESTAMP)");
|
||||||
|
sql.execute("CREATE TABLE fl_outbound_dispatch(container_booking_id BIGINT,confirmed_at TIMESTAMP,reversed_at TIMESTAMP,status VARCHAR)");
|
||||||
|
sql.execute("INSERT INTO fl_container_booking(id,destination_port_name,destination_port_code,record_status) VALUES " +
|
||||||
|
"(1,'洛杉矶','USLAX','ACTIVE'),(2,NULL,'USLAX','ACTIVE'),(3,' ','USLAX','ACTIVE')," +
|
||||||
|
"(4,'洛杉矶',NULL,'ACTIVE'),(5,'',' ','ACTIVE')");
|
||||||
|
sql.execute("INSERT INTO fl_outbound_dispatch SELECT id,'2026-10-01 00:00:00',NULL,'WAITING_FOR_LOADING' FROM fl_container_booking");
|
||||||
|
}
|
||||||
|
var config = new MybatisConfiguration();
|
||||||
|
config.setEnvironment(new Environment("test", new JdbcTransactionFactory(), ds));
|
||||||
|
config.addMapper(ContainerCalendarSourceMapper.class);
|
||||||
|
try (var session = new MybatisSqlSessionFactoryBuilder().build(config).openSession()) {
|
||||||
|
var mapper = session.getMapper(ContainerCalendarSourceMapper.class);
|
||||||
|
var from = LocalDateTime.parse("2026-10-01T00:00:00");
|
||||||
|
var calendar = mapper.sources(from, from.minusDays(2), from.minusDays(4), from.plusMonths(1));
|
||||||
|
var explicit = mapper.byIds(java.util.List.of(1L, 2L, 3L, 4L, 5L));
|
||||||
|
var expected = java.util.Arrays.asList("洛杉矶", "USLAX", "USLAX", "洛杉矶", null);
|
||||||
|
assertEquals(expected, calendar.stream().map(row -> row.destinationPortName()).toList());
|
||||||
|
for (var row : explicit) {
|
||||||
|
assertEquals(expected.get(row.containerBookingId().intValue() - 1), row.destinationPortName());
|
||||||
|
}
|
||||||
|
assertEquals(5, explicit.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 后续观测可将实际到港修正为更早日期,两入口须取同一条最新实际记录。 */
|
/** 后续观测可将实际到港修正为更早日期,两入口须取同一条最新实际记录。 */
|
||||||
@Test
|
@Test
|
||||||
void usesLatestObservedArrivalIncludingSameObservationIdTieBreakAcrossBothEntrypoints() throws Exception {
|
void usesLatestObservedArrivalIncludingSameObservationIdTieBreakAcrossBothEntrypoints() throws Exception {
|
||||||
@@ -18,7 +53,7 @@ class ContainerCalendarSourceMapperTest {
|
|||||||
ds.setURL("jdbc:h2:mem:arrival-source-correction;MODE=MySQL;DB_CLOSE_DELAY=-1");
|
ds.setURL("jdbc:h2:mem:arrival-source-correction;MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||||
try (var connection = ds.getConnection(); var sql = connection.createStatement()) {
|
try (var connection = ds.getConnection(); var sql = connection.createStatement()) {
|
||||||
sql.execute("CREATE TABLE fl_container_booking(id BIGINT PRIMARY KEY,cabinet_no VARCHAR,container_no VARCHAR," +
|
sql.execute("CREATE TABLE fl_container_booking(id BIGINT PRIMARY KEY,cabinet_no VARCHAR,container_no VARCHAR," +
|
||||||
"vessel_name VARCHAR,voyage_no VARCHAR,destination_port_name VARCHAR,destination_warehouse_code VARCHAR," +
|
"vessel_name VARCHAR,voyage_no VARCHAR,destination_port_name VARCHAR,destination_port_code VARCHAR,destination_warehouse_code VARCHAR," +
|
||||||
"actual_port_arrival_at TIMESTAMP,estimated_port_arrival_at TIMESTAMP,unloading_completed_at TIMESTAMP,record_status VARCHAR)");
|
"actual_port_arrival_at TIMESTAMP,estimated_port_arrival_at TIMESTAMP,unloading_completed_at TIMESTAMP,record_status VARCHAR)");
|
||||||
sql.execute("CREATE TABLE fl_terminal_drayage(container_booking_id BIGINT,actual_empty_return_at TIMESTAMP)");
|
sql.execute("CREATE TABLE fl_terminal_drayage(container_booking_id BIGINT,actual_empty_return_at TIMESTAMP)");
|
||||||
sql.execute("CREATE TABLE fl_carrier_schedule_query(id BIGINT,version INT,status VARCHAR)");
|
sql.execute("CREATE TABLE fl_carrier_schedule_query(id BIGINT,version INT,status VARCHAR)");
|
||||||
@@ -65,7 +100,7 @@ class ContainerCalendarSourceMapperTest {
|
|||||||
ds.setURL("jdbc:h2:mem:four-type-calendar;MODE=MySQL;DB_CLOSE_DELAY=-1");
|
ds.setURL("jdbc:h2:mem:four-type-calendar;MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||||
try (var connection = ds.getConnection(); var sql = connection.createStatement()) {
|
try (var connection = ds.getConnection(); var sql = connection.createStatement()) {
|
||||||
sql.execute("CREATE TABLE fl_container_booking(id BIGINT,cabinet_no VARCHAR,container_no VARCHAR," +
|
sql.execute("CREATE TABLE fl_container_booking(id BIGINT,cabinet_no VARCHAR,container_no VARCHAR," +
|
||||||
"vessel_name VARCHAR,voyage_no VARCHAR,destination_port_name VARCHAR,destination_warehouse_code VARCHAR," +
|
"vessel_name VARCHAR,voyage_no VARCHAR,destination_port_name VARCHAR,destination_port_code VARCHAR,destination_warehouse_code VARCHAR," +
|
||||||
"actual_port_arrival_at TIMESTAMP,estimated_port_arrival_at TIMESTAMP,unloading_completed_at TIMESTAMP,record_status VARCHAR)");
|
"actual_port_arrival_at TIMESTAMP,estimated_port_arrival_at TIMESTAMP,unloading_completed_at TIMESTAMP,record_status VARCHAR)");
|
||||||
sql.execute("CREATE TABLE fl_terminal_drayage(container_booking_id BIGINT,actual_empty_return_at TIMESTAMP)");
|
sql.execute("CREATE TABLE fl_terminal_drayage(container_booking_id BIGINT,actual_empty_return_at TIMESTAMP)");
|
||||||
sql.execute("CREATE TABLE fl_carrier_schedule_node_fact(id BIGINT,container_booking_id BIGINT,standard_node VARCHAR,actual_at TIMESTAMP,observed_at TIMESTAMP)");
|
sql.execute("CREATE TABLE fl_carrier_schedule_node_fact(id BIGINT,container_booking_id BIGINT,standard_node VARCHAR,actual_at TIMESTAMP,observed_at TIMESTAMP)");
|
||||||
|
|||||||
+4
-2
@@ -11,6 +11,8 @@ import java.util.List;
|
|||||||
public interface DispatchCandidateMapper {
|
public interface DispatchCandidateMapper {
|
||||||
/** 自动预留托盘不是实收;尚未开始收货时仍以计划来源预排,不生成库存或实托装载事实。 */
|
/** 自动预留托盘不是实收;尚未开始收货时仍以计划来源预排,不生成库存或实托装载事实。 */
|
||||||
String PLAN_ONLY = "(p.warehouse_pallet_id IS NULL OR (COALESCE(p.assignment_status,'')='RESERVED' AND COALESCE(p.actual_package_count,0)=0))";
|
String PLAN_ONLY = "(p.warehouse_pallet_id IS NULL OR (COALESCE(p.assignment_status,'')='RESERVED' AND COALESCE(p.actual_package_count,0)=0))";
|
||||||
|
/** 完成入库或已确认短少后,不再把未实收的预留托盘当作在途货;真实库存仍可排车。 */
|
||||||
|
String OPEN_PLAN = "COALESCE(ip.status,'') NOT IN ('COMPLETED','PARTIAL_INBOUND')";
|
||||||
/** 在途计划的全部真实来源必须已离港;人工节点覆盖后不再借用原柜节点。 */
|
/** 在途计划的全部真实来源必须已离港;人工节点覆盖后不再借用原柜节点。 */
|
||||||
String DEPARTED_PLAN =
|
String DEPARTED_PLAN =
|
||||||
"EXISTS (SELECT 1 FROM ll_inbound_plan_pallet_allocation da JOIN ll_inbound_plan_line dl " +
|
"EXISTS (SELECT 1 FROM ll_inbound_plan_pallet_allocation da JOIN ll_inbound_plan_line dl " +
|
||||||
@@ -178,7 +180,7 @@ public interface DispatchCandidateMapper {
|
|||||||
"JOIN ll_inbound_plan_line hl ON hl.id=ha.line_id " +
|
"JOIN ll_inbound_plan_line hl ON hl.id=ha.line_id " +
|
||||||
"JOIN ll_order ho ON ho.id=hl.order_id AND ho.customer_id=ip.customer_id " +
|
"JOIN ll_order ho ON ho.id=hl.order_id AND ho.customer_id=ip.customer_id " +
|
||||||
"WHERE ha.pallet_id=p.id AND ho.detained=1) " +
|
"WHERE ha.pallet_id=p.id AND ho.detained=1) " +
|
||||||
"AND (NOT " + PLAN_ONLY + " OR (" + DEPARTED_PLAN + ")) " +
|
"AND (NOT " + PLAN_ONLY + " OR (" + OPEN_PLAN + " AND " + DEPARTED_PLAN + ")) " +
|
||||||
"AND (" + PLAN_ONLY + " OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
"AND (" + PLAN_ONLY + " OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
||||||
"WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
"WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
||||||
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock d " +
|
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock d " +
|
||||||
@@ -283,7 +285,7 @@ public interface DispatchCandidateMapper {
|
|||||||
"JOIN ll_inbound_plan_line hl ON hl.id=ha.line_id " +
|
"JOIN ll_inbound_plan_line hl ON hl.id=ha.line_id " +
|
||||||
"JOIN ll_order ho ON ho.id=hl.order_id AND ho.customer_id=ip.customer_id " +
|
"JOIN ll_order ho ON ho.id=hl.order_id AND ho.customer_id=ip.customer_id " +
|
||||||
"WHERE ha.pallet_id=p.id AND ho.detained=1) " +
|
"WHERE ha.pallet_id=p.id AND ho.detained=1) " +
|
||||||
"AND (NOT " + PLAN_ONLY + " OR (" + DEPARTED_PLAN + ")) " +
|
"AND (NOT " + PLAN_ONLY + " OR (" + OPEN_PLAN + " AND " + DEPARTED_PLAN + ")) " +
|
||||||
"AND (" + PLAN_ONLY + " OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
"AND (" + PLAN_ONLY + " OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
||||||
"WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
"WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
||||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||||
|
|||||||
+30
-20
@@ -16,12 +16,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
class DispatchCandidateMapperContractTest {
|
class DispatchCandidateMapperContractTest {
|
||||||
/** 执行正式计划/实收判定与离港门禁,覆盖自动预留、人工覆盖、混合来源及库存耗尽。 */
|
/** 执行正式候选和提交资格,覆盖在途预排、收货结束后的空预留托盘及真实库存。 */
|
||||||
@Test
|
@Test
|
||||||
void reservedPalletsCanBePlannedAfterDepartureWithoutInventingReceivedStock() throws Exception {
|
void reservedPalletsCanBePlannedAfterDepartureWithoutInventingReceivedStock() throws Exception {
|
||||||
try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-prearrival;MODE=MySQL")) {
|
try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-prearrival;MODE=MySQL")) {
|
||||||
var db = connection.createStatement();
|
var db = connection.createStatement();
|
||||||
db.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR)");
|
db.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR,status VARCHAR DEFAULT 'PDA_PENDING')");
|
||||||
db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,plan_id BIGINT,warehouse_pallet_id BIGINT,assignment_status VARCHAR,actual_package_count INT)");
|
db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,plan_id BIGINT,warehouse_pallet_id BIGINT,assignment_status VARCHAR,actual_package_count INT)");
|
||||||
db.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT,customer_id VARCHAR,order_id BIGINT)");
|
db.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT,customer_id VARCHAR,order_id BIGINT)");
|
||||||
db.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT)");
|
db.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT)");
|
||||||
@@ -29,8 +29,8 @@ class DispatchCandidateMapperContractTest {
|
|||||||
db.execute("CREATE TABLE ll_order_manual_node(order_id BIGINT,customer_id VARCHAR,node_code VARCHAR,node_at TIMESTAMP)");
|
db.execute("CREATE TABLE ll_order_manual_node(order_id BIGINT,customer_id VARCHAR,node_code VARCHAR,node_at TIMESTAMP)");
|
||||||
db.execute("CREATE TABLE ll_container_shipping_gate(container_booking_id BIGINT,node_sequence INT,actual_at TIMESTAMP)");
|
db.execute("CREATE TABLE ll_container_shipping_gate(container_booking_id BIGINT,node_sequence INT,actual_at TIMESTAMP)");
|
||||||
db.execute("CREATE TABLE ll_inventory_position(customer_id VARCHAR,pallet_id BIGINT,available_packages INT)");
|
db.execute("CREATE TABLE ll_inventory_position(customer_id VARCHAR,pallet_id BIGINT,available_packages INT)");
|
||||||
for (int id = 1; id <= 9; id++) {
|
for (int id = 1; id <= 14; id++) {
|
||||||
db.execute("INSERT INTO ll_inbound_plan VALUES (" + id + ",'A')");
|
db.execute("INSERT INTO ll_inbound_plan(id,customer_id) VALUES (" + id + ",'A')");
|
||||||
db.execute("INSERT INTO ll_inbound_plan_pallet VALUES (" + id + "," + id + "," + (100 + id) + ",'RESERVED',0)");
|
db.execute("INSERT INTO ll_inbound_plan_pallet VALUES (" + id + "," + id + "," + (100 + id) + ",'RESERVED',0)");
|
||||||
db.execute("INSERT INTO ll_inbound_plan_line VALUES (" + id + ",'A'," + id + ")");
|
db.execute("INSERT INTO ll_inbound_plan_line VALUES (" + id + ",'A'," + id + ")");
|
||||||
db.execute("INSERT INTO ll_inbound_plan_pallet_allocation VALUES (" + id + "," + id + ")");
|
db.execute("INSERT INTO ll_inbound_plan_pallet_allocation VALUES (" + id + "," + id + ")");
|
||||||
@@ -44,24 +44,34 @@ class DispatchCandidateMapperContractTest {
|
|||||||
db.execute("INSERT INTO ll_order_manual_node VALUES (7,'A','ACTUAL_DEPARTURE',CURRENT_TIMESTAMP),(9,'B','ACTUAL_DEPARTURE',CURRENT_TIMESTAMP)");
|
db.execute("INSERT INTO ll_order_manual_node VALUES (7,'A','ACTUAL_DEPARTURE',CURRENT_TIMESTAMP),(9,'B','ACTUAL_DEPARTURE',CURRENT_TIMESTAMP)");
|
||||||
db.execute("INSERT INTO ll_inbound_plan_line VALUES (20,'A',2)");
|
db.execute("INSERT INTO ll_inbound_plan_line VALUES (20,'A',2)");
|
||||||
db.execute("INSERT INTO ll_inbound_plan_pallet_allocation VALUES (8,20)");
|
db.execute("INSERT INTO ll_inbound_plan_pallet_allocation VALUES (8,20)");
|
||||||
|
// 同一计划原分两托,最终21箱全收在一托;另一预留托盘不能继续冒充可预排货物。
|
||||||
|
db.execute("UPDATE ll_inbound_plan SET status='COMPLETED' WHERE id IN (10,13,14)");
|
||||||
|
db.execute("UPDATE ll_inbound_plan_pallet SET plan_id=10,assignment_status='ASSIGNED',actual_package_count=21 WHERE id=11");
|
||||||
|
db.execute("INSERT INTO ll_inventory_position VALUES ('A',111,21)");
|
||||||
|
db.execute("UPDATE ll_inbound_plan SET status='PARTIAL_INBOUND' WHERE id=12");
|
||||||
|
db.execute("UPDATE ll_inbound_plan_pallet SET warehouse_pallet_id=NULL WHERE id=13");
|
||||||
|
db.execute("UPDATE ll_inbound_plan_pallet SET assignment_status='ASSIGNED' WHERE id=14");
|
||||||
String plan = DispatchCandidateMapper.PLAN_ONLY;
|
String plan = DispatchCandidateMapper.PLAN_ONLY;
|
||||||
String eligible = "AND (NOT " + plan + " OR (" + DispatchCandidateMapper.DEPARTED_PLAN + ")) "
|
// 直接执行两条生产SQL中的资格片段,避免仅验证测试自行拼出的规则。
|
||||||
+ "AND (" + plan + " OR EXISTS (SELECT 1 FROM ll_inventory_position pos "
|
|
||||||
+ "WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0))";
|
|
||||||
var ids = new ArrayList<Long>();
|
|
||||||
try (var rows = db.executeQuery("SELECT p.id,CASE WHEN " + plan + " THEN NULL ELSE p.warehouse_pallet_id END AS physical,"
|
|
||||||
+ "CASE WHEN " + plan + " THEN 'PLAN' ELSE 'RECEIVED' END AS stage "
|
|
||||||
+ "FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id WHERE 1=1 " + eligible + " ORDER BY p.id")) {
|
|
||||||
while (rows.next()) {
|
|
||||||
long id = rows.getLong("id"); ids.add(id);
|
|
||||||
assertEquals(id == 3 ? "RECEIVED" : "PLAN", rows.getString("stage"));
|
|
||||||
assertEquals(id == 3 ? 103L : null, rows.getObject("physical", Long.class));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertEquals(List.of(1L, 3L, 7L), ids);
|
|
||||||
// 查询和写入选中来源共用同一判定;不允许仅在列表展示在途但提交时又要求实收。
|
|
||||||
for (String sql : List.of(DispatchCandidateMapper.AVAILABLE_PLAN_BODY, DispatchCandidateMapper.SELECTED_PLAN_SQL)) {
|
for (String sql : List.of(DispatchCandidateMapper.AVAILABLE_PLAN_BODY, DispatchCandidateMapper.SELECTED_PLAN_SQL)) {
|
||||||
assertTrue(sql.contains(eligible.replace("> 0", "> 0").replace("pos.customer_id=ip.customer_id", sql.contains("WHERE pos.customer_id=#{customerId}") ? "pos.customer_id=#{customerId}" : "pos.customer_id=ip.customer_id")));
|
int start = sql.indexOf("AND (NOT " + plan + " OR (");
|
||||||
|
int stockGate = sql.indexOf("AND (" + plan + " OR EXISTS", start);
|
||||||
|
int end = sql.indexOf("AND NOT EXISTS (SELECT 1 FROM ll_", stockGate);
|
||||||
|
String eligible = sql.substring(start, end).replace(">", ">")
|
||||||
|
.replace("#{customerId}", "'A'");
|
||||||
|
var ids = new ArrayList<Long>();
|
||||||
|
try (var rows = db.executeQuery("SELECT p.id,p.actual_package_count,CASE WHEN " + plan + " THEN NULL ELSE p.warehouse_pallet_id END AS physical,"
|
||||||
|
+ "CASE WHEN " + plan + " THEN 'PLAN' ELSE 'RECEIVED' END AS stage "
|
||||||
|
+ "FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id WHERE 1=1 " + eligible + " ORDER BY p.id")) {
|
||||||
|
while (rows.next()) {
|
||||||
|
long id = rows.getLong("id"); ids.add(id);
|
||||||
|
assertEquals(id == 3 || id == 11 ? "RECEIVED" : "PLAN", rows.getString("stage"));
|
||||||
|
assertEquals(id == 3 || id == 11 ? 100L + id : null, rows.getObject("physical", Long.class));
|
||||||
|
if (id == 11) assertEquals(21, rows.getInt("actual_package_count"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 完成入库或已确认短少后,空预留托盘不可排车,真实实收托盘继续按库存校验。
|
||||||
|
assertEquals(List.of(1L, 3L, 7L, 11L), ids);
|
||||||
assertTrue(sql.contains("CASE WHEN " + plan + " THEN NULL ELSE p.warehouse_pallet_id END AS warehousePalletId"));
|
assertTrue(sql.contains("CASE WHEN " + plan + " THEN NULL ELSE p.warehouse_pallet_id END AS warehousePalletId"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user