提交
This commit is contained in:
+14
@@ -44,6 +44,20 @@ class InternalInboundOrderReferenceControllerTest {
|
||||
@Resource
|
||||
private InternalInboundOrderController controller;
|
||||
|
||||
/** 新增状态查询和摘要入口都必须校验内部服务身份。 */
|
||||
@Test
|
||||
void statusEndpointsRequireInternalTokenBeforeDelegation() {
|
||||
var request = new UnifiedInboundQuery(10001L, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, List.of(77L), null, null, 1, 20, null);
|
||||
assertThatThrownBy(() -> controller.unifiedStatusRows(null, request)).isInstanceOf(BizException.class);
|
||||
assertThatThrownBy(() -> controller.unifiedStatusSummaries("wrong", request)).isInstanceOf(BizException.class);
|
||||
org.mockito.Mockito.verifyNoInteractions(unifiedQueryService);
|
||||
when(unifiedQueryService.statusRows(request)).thenReturn(List.of());
|
||||
when(unifiedQueryService.statusSummaries(request)).thenReturn(List.of());
|
||||
assertThat(controller.unifiedStatusRows("internal-token", request).data()).isEmpty();
|
||||
assertThat(controller.unifiedStatusSummaries("internal-token", request).data()).isEmpty();
|
||||
}
|
||||
|
||||
@Test void workbenchFiltersRejectMissingAndWrongInternalIdentity() {
|
||||
var request = new com.baiyue.firstleg.dto.MerchantWorkbenchFilters.Request(11L, "LOADED", null);
|
||||
assertThatThrownBy(() -> controller.workbenchFilters(null, request)).isInstanceOf(BizException.class);
|
||||
|
||||
+10
@@ -9,6 +9,16 @@ import java.nio.file.Path;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LoadingCompletionSqlContractTest {
|
||||
/** 后执行的渠道柜次脚本不能恢复旧触发器并阻断持久化完成命令。 */
|
||||
@Test
|
||||
void channelMigrationKeepsTheDurableCompletionTransitions() throws Exception {
|
||||
String sql = Files.readString(Path.of("src", "main", "resources", "db",
|
||||
"phase3-channel-container-migration.sql"), StandardCharsets.UTF_8);
|
||||
assertThat(sql).contains("OLD.task_status = 'LOADING' AND NEW.task_status IN ('LOADING','COMPLETING')")
|
||||
.contains("OLD.task_status = 'COMPLETING' AND NEW.task_status IN ('COMPLETING','COMPLETION_FAILED','COMPLETED')")
|
||||
.contains("OLD.task_status = 'COMPLETION_FAILED' AND NEW.task_status = 'COMPLETING'")
|
||||
.doesNotContain("任务状态只允许完成");
|
||||
}
|
||||
@Test
|
||||
void baselineAndUnifiedSqlDefineDurableCompletionStateMachine() throws Exception {
|
||||
assertContract(Files.readString(Path.of("src", "main", "resources", "db",
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
@Test
|
||||
void initializationPassIsUniqueAndFinal() throws Exception {
|
||||
String sql = sql();
|
||||
int expectedTables = 227;
|
||||
int expectedTables = 228;
|
||||
long actualTables = java.util.regex.Pattern.compile("(?m)^CREATE TABLE\\s+")
|
||||
.matcher(sql).results().count();
|
||||
assertThat(actualTables).isEqualTo(expectedTables);
|
||||
|
||||
+11
@@ -69,6 +69,17 @@ class UnifiedInboundTaxModePaginationTest {
|
||||
assertPage(null, 1, 4, 6);
|
||||
assertPage(null, 2, 4, 4);
|
||||
}
|
||||
/** 轻量统计读取全量状态,不能把第二页当作全量,也不能越过客户和软删除条件。 */
|
||||
@Test
|
||||
void statusProjectionUsesFullFilteredSetAndEmptyCandidatesStayEmpty() {
|
||||
var query = new UnifiedInboundQuery(11L, null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null, 2, 1, "TAX_REFUND");
|
||||
assertThat(repository.unifiedStatusEntities(query)).extracting(InboundOrderEntity::getId).containsExactly(6L, 2L);
|
||||
var empty = new UnifiedInboundQuery(11L, null, null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, List.of(), null, null, 2, 1, "TAX_REFUND");
|
||||
assertThat(repository.unifiedStatusEntities(empty)).isEmpty();
|
||||
}
|
||||
|
||||
private void assertPage(String taxMode, long page, long total, long expectedId) {
|
||||
var query = new UnifiedInboundQuery(11L, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, page, 1, taxMode);
|
||||
var result = repository.pageUnifiedEntities(query, List.of("LOADED"));
|
||||
|
||||
@@ -50,6 +50,18 @@ class ContainerServiceTest {
|
||||
@MockBean private SmartLoadingService smartLoadingService;
|
||||
@Resource private ContainerService service;
|
||||
|
||||
/** 实际开船可独立打开清关资料,预估节点与缺失货柜不能绕过校验。 */
|
||||
@Test void clearanceAcceptsActualSailingWithoutInventingOutboundConfirmation() {
|
||||
when(repository.find(91L)).thenReturn(new ContainerBookingEntity());
|
||||
assertThatThrownBy(() -> service.requireClearanceAccess(91L)).isInstanceOf(BizException.class);
|
||||
when(repository.sailed(91L)).thenReturn(true);
|
||||
service.requireClearanceAccess(91L);
|
||||
assertThatThrownBy(() -> service.requireClearanceAccess(92L)).isInstanceOf(BizException.class);
|
||||
when(repository.sailed(91L)).thenReturn(false);
|
||||
when(repository.departed(91L)).thenReturn(true);
|
||||
service.requireClearanceAccess(91L);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUpWarehouse() {
|
||||
when(documentNumbers.nextCabinetNo()).thenReturn("D0001");
|
||||
|
||||
+14
@@ -9,6 +9,20 @@ import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class MerchantWorkbenchFilterServiceTest {
|
||||
/** 金额待确认与计费重待确认不同,客户工作台只能使用后者。 */
|
||||
@Test void customerQueueUsesIndependentWeightConfirmation() {
|
||||
var mapper=mock(MerchantWorkbenchFilterMapper.class);
|
||||
var weights=mock(com.baiyue.firstleg.workflow.ChargeableWeightService.class);
|
||||
var service=new MerchantWorkbenchFilterService();
|
||||
ReflectionTestUtils.setField(service,"mapper",mapper);
|
||||
ReflectionTestUtils.setField(service,"weights",weights);
|
||||
when(weights.pendingOrders(11)).thenReturn(List.of(101L));
|
||||
var response=service.read(new MerchantWorkbenchFilters.Request(11L,null,null));
|
||||
assertThat(response.feeOrderIds()).containsExactly(101L);
|
||||
verify(weights).pendingOrders(11);
|
||||
verify(mapper,never()).pendingFees(anyLong());
|
||||
}
|
||||
|
||||
@Test void exactDetailProgressSkipsUnrelatedCustomerQueues() {
|
||||
var mapper=mock(MerchantWorkbenchFilterMapper.class);
|
||||
var unified=mock(UnifiedInboundQueryService.class);
|
||||
|
||||
+67
@@ -24,11 +24,77 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class UnifiedInboundQueryServiceTest {
|
||||
/** 直接内部查询同样先按业务事实筛选再分页,不能返回未命中的订单。 */
|
||||
@Test
|
||||
void workflowPageFiltersBeforePagination() {
|
||||
var repository = mock(InboundOrderRepository.class);
|
||||
var workflow = mock(com.baiyue.firstleg.workflow.WorkflowTabService.class);
|
||||
var service = new UnifiedInboundQueryService();
|
||||
ReflectionTestUtils.setField(service, "inbounds", repository);
|
||||
ReflectionTestUtils.setField(service, "workflowTabs", workflow);
|
||||
ReflectionTestUtils.setField(service, "operations", mock(UnifiedInboundReadRepository.class));
|
||||
ReflectionTestUtils.setField(service, "pricing", mock(FbaPricingRepository.class));
|
||||
var query = new UnifiedInboundQuery(10001L, null, null, "WAIT_WEIGHT_CONFIRM", null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, null, 2, 1, null);
|
||||
var first = new InboundOrderEntity(); first.setId(1L); first.setTenantId(10001L); first.setOrderId(77L); first.setProcessStatus("WAIT_SPLIT");
|
||||
var second = new InboundOrderEntity(); second.setId(2L); second.setTenantId(10001L); second.setOrderId(78L); second.setProcessStatus("WAIT_SPLIT");
|
||||
var other = new InboundOrderEntity(); other.setId(3L); other.setTenantId(10001L); other.setOrderId(79L); other.setProcessStatus("WAIT_SPLIT");
|
||||
var sources = List.of(first, second, other);
|
||||
when(repository.unifiedStatusEntities(query)).thenReturn(sources);
|
||||
when(workflow.rows(sources)).thenReturn(List.of(
|
||||
new UnifiedInboundQueryService.StatusRow(10001L,77L,"WAIT_WEIGHT_CONFIRM"),
|
||||
new UnifiedInboundQueryService.StatusRow(10001L,78L,"WAIT_WEIGHT_CONFIRM"),
|
||||
new UnifiedInboundQueryService.StatusRow(10001L,79L,"WEIGHT_CONFIRMED")));
|
||||
when(repository.unifiedMatchedEntities(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.eq(List.of())))
|
||||
.thenReturn(sources);
|
||||
var result = service.page(query);
|
||||
assertThat(result.total()).isEqualTo(2);
|
||||
assertThat(result.records()).singleElement().satisfies(row -> assertThat(row.inbound().orderId()).isEqualTo(78));
|
||||
}
|
||||
|
||||
/** 分片订单保留各状态命中,同一状态内去重;不触发逐票富化查询。 */
|
||||
@Test
|
||||
void statusRowsDeduplicateLatestMatchingInboundWithoutEnrichment() {
|
||||
var repository = mock(InboundOrderRepository.class);
|
||||
var operations = mock(UnifiedInboundReadRepository.class);
|
||||
var pricing = mock(FbaPricingRepository.class);
|
||||
var service = new UnifiedInboundQueryService();
|
||||
ReflectionTestUtils.setField(service, "workflowTabs", mock(com.baiyue.firstleg.workflow.WorkflowTabService.class));
|
||||
ReflectionTestUtils.setField(service, "inbounds", repository);
|
||||
ReflectionTestUtils.setField(service, "operations", operations);
|
||||
ReflectionTestUtils.setField(service, "pricing", pricing);
|
||||
var query = new UnifiedInboundQuery(10001L, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, null, 9, 1, null);
|
||||
var newest = new InboundOrderEntity(); newest.setTenantId(10001L); newest.setOrderId(77L);
|
||||
newest.setProcessStatus("MEASURE_PENDING");
|
||||
var older = new InboundOrderEntity(); older.setTenantId(10001L); older.setOrderId(77L);
|
||||
older.setProcessStatus("LOADED");
|
||||
when(repository.unifiedStatusEntities(query)).thenReturn(List.of(newest, newest, older));
|
||||
assertThat(service.statusRows(query)).containsExactly(
|
||||
new UnifiedInboundQueryService.StatusRow(10001L, 77L, "WAIT_MEASURE"),
|
||||
new UnifiedInboundQueryService.StatusRow(10001L, 77L, "LOADED"));
|
||||
verifyNoInteractions(operations, pricing);
|
||||
}
|
||||
|
||||
/** 摘要入口必须提供当前页候选,不能误触发全量明细读取。 */
|
||||
@Test
|
||||
void statusSummariesRejectUnboundedCandidateRequest() {
|
||||
var repository = mock(InboundOrderRepository.class);
|
||||
var service = new UnifiedInboundQueryService();
|
||||
ReflectionTestUtils.setField(service, "workflowTabs", mock(com.baiyue.firstleg.workflow.WorkflowTabService.class));
|
||||
ReflectionTestUtils.setField(service, "inbounds", repository);
|
||||
var query = new UnifiedInboundQuery(10001L, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, null, 1, 20, null);
|
||||
assertThatThrownBy(() -> service.statusSummaries(query)).isInstanceOf(BizException.class);
|
||||
verifyNoInteractions(repository);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", " ", "tax_refund", "TAX_REFUND ", "OTHER", "TAX_INCLUDED"})
|
||||
void illegalTaxModeFailsBeforeRepository(String taxMode) {
|
||||
var repository = mock(InboundOrderRepository.class);
|
||||
var service = new UnifiedInboundQueryService();
|
||||
ReflectionTestUtils.setField(service, "workflowTabs", mock(com.baiyue.firstleg.workflow.WorkflowTabService.class));
|
||||
ReflectionTestUtils.setField(service, "inbounds", repository);
|
||||
var query = new UnifiedInboundQuery(10001L, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null, null, null, null, 1, 20, taxMode);
|
||||
@@ -144,6 +210,7 @@ class UnifiedInboundQueryServiceTest {
|
||||
var operations = mock(UnifiedInboundReadRepository.class);
|
||||
var pricing = mock(FbaPricingRepository.class);
|
||||
var service = new UnifiedInboundQueryService();
|
||||
ReflectionTestUtils.setField(service, "workflowTabs", mock(com.baiyue.firstleg.workflow.WorkflowTabService.class));
|
||||
ReflectionTestUtils.setField(service,"inbounds",inbounds);
|
||||
ReflectionTestUtils.setField(service,"operations",operations);
|
||||
ReflectionTestUtils.setField(service,"pricing",pricing);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.baiyue.firstleg.workflow;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** 跨服务缺项和客户归属异常不能生成看似成功的部分统计。 */
|
||||
class WorkflowBoundaryTest {
|
||||
/** 真实开船来源的测试事实。 */
|
||||
private WorkflowFacts.Fact fact() {
|
||||
return new WorkflowFacts.Fact(11,1,101,"APPROVED",BigDecimal.TEN,
|
||||
LocalDateTime.of(2026,10,19,10,0),true,false,false,false,null,null);
|
||||
}
|
||||
|
||||
/** 下游漏来源或返回其他客户均整体失败。 */
|
||||
@Test void rejectsMissingAndForeignDownstreamFacts() {
|
||||
var repository=mock(WorkflowFactRepository.class);
|
||||
var downstream=mock(LastlegWorkflowClient.class);
|
||||
var service=new WorkflowTabService(repository,downstream);
|
||||
var inbound=new InboundOrderEntity();inbound.setId(1L);inbound.setTenantId(11L);
|
||||
inbound.setOrderId(101L);inbound.setProcessStatus("LOADED");
|
||||
when(repository.facts(List.of(1L))).thenReturn(List.of(fact()));
|
||||
when(downstream.read(any(),any())).thenReturn(ApiResponse.success(List.of()));
|
||||
assertThatThrownBy(()->service.rows(List.of(inbound))).isInstanceOf(BizException.class);
|
||||
when(downstream.read(any(),any())).thenReturn(ApiResponse.success(List.of(
|
||||
new WorkflowFacts.Overseas(22,1,true,true,false,true))));
|
||||
assertThatThrownBy(()->service.rows(List.of(inbound))).isInstanceOf(BizException.class);
|
||||
when(downstream.read(any(),any())).thenThrow(new IllegalStateException("unavailable"));
|
||||
assertThatThrownBy(()->service.rows(List.of(inbound))).isInstanceOf(BizException.class);
|
||||
}
|
||||
|
||||
/** 重测同值也要求重新确认,金额没有参与摘要。 */
|
||||
@Test void repeatedMeasurementRequiresNewConfirmation() {
|
||||
var original=fact();
|
||||
var measuredAgain=new WorkflowFacts.Fact(11,1,101,"APPROVED",BigDecimal.TEN,
|
||||
original.measureCompletedAt().plusSeconds(1),true,false,false,false,
|
||||
ChargeableWeightService.fingerprint(original),LocalDateTime.now());
|
||||
assertThat(ChargeableWeightService.weight(measuredAgain).confirmed()).isFalse();
|
||||
}
|
||||
|
||||
/** 平台主体不得冒充客户,客户和操作者只取认证上下文。 */
|
||||
@Test void confirmationUsesAuthenticatedCustomerAndOperator() {
|
||||
var service=mock(ChargeableWeightService.class);
|
||||
var controller=new MerchantChargeableWeightController(service);
|
||||
var platform=new com.baiyue.common.security.AuthenticatedUser(7L,0L,0L,9L,"platform","PLATFORM","WEB",java.util.Set.of());
|
||||
var request=new WorkflowFacts.Confirm("a".repeat(64));
|
||||
assertThatThrownBy(()->controller.read(platform,101)).isInstanceOf(BizException.class);
|
||||
assertThatThrownBy(()->controller.confirm(platform,1,"key",request)).isInstanceOf(BizException.class);
|
||||
verifyNoInteractions(service);
|
||||
var merchant=new com.baiyue.common.security.AuthenticatedUser(7L,11L,0L,9L,"member","MERCHANT","WEB",java.util.Set.of());
|
||||
controller.read(merchant,101);
|
||||
controller.confirm(merchant,1,"key",request);
|
||||
verify(service).read(11,101);
|
||||
verify(service).confirm(11,7,1,"key",request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.baiyue.firstleg.workflow;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
/** 取消回显、分柜隔离和独立计费重版本回归。 */
|
||||
class WorkflowBusinessTest {
|
||||
/** 报关资料齐备进入汇总,实际开船及清关资料分别驱动后续页签。 */
|
||||
@Test void customsDocumentsDepartureAndClearanceDriveTabs() {
|
||||
var time=LocalDateTime.of(2026,10,19,10,0);
|
||||
var missing=new WorkflowFacts.Fact(11,1,101,"APPROVED",BigDecimal.TEN,time,false,false,true,false,null,null);
|
||||
var uploaded=new WorkflowFacts.Fact(11,1,101,"APPROVED",BigDecimal.TEN,time,false,false,false,true,null,null);
|
||||
var departed=new WorkflowFacts.Fact(11,1,101,"APPROVED",BigDecimal.TEN,time,true,false,false,true,null,null);
|
||||
var cleared=new WorkflowFacts.Fact(11,1,101,"APPROVED",BigDecimal.TEN,time,true,true,false,true,null,null);
|
||||
var uploadedBeforeDeparture=new WorkflowFacts.Fact(11,1,101,"APPROVED",BigDecimal.TEN,time,false,true,false,true,null,null);
|
||||
var pendingUnload=new WorkflowFacts.Overseas(11,1,false,false,false,false);
|
||||
var unloaded=new WorkflowFacts.Overseas(11,1,false,false,false,true);
|
||||
assertThat(WorkflowTabService.classify(missing,"LOADED",null))
|
||||
.contains("WAIT_CUSTOMS_DOCS").doesNotContain("EXPORT_CUSTOMS","SAILING_CLEARANCE","WAIT_UNLOAD");
|
||||
assertThat(WorkflowTabService.classify(uploaded,"LOADED",null))
|
||||
.contains("EXPORT_CUSTOMS").doesNotContain("WAIT_CUSTOMS_DOCS","SAILING_CLEARANCE","WAIT_UNLOAD");
|
||||
assertThat(WorkflowTabService.classify(uploadedBeforeDeparture,"LOADED",pendingUnload))
|
||||
.contains("EXPORT_CUSTOMS").doesNotContain("SAILING_CLEARANCE","WAIT_UNLOAD");
|
||||
assertThat(WorkflowTabService.classify(departed,"LOADED",pendingUnload))
|
||||
.contains("SAILING_CLEARANCE").doesNotContain("WAIT_CUSTOMS_DOCS","EXPORT_CUSTOMS","WAIT_UNLOAD");
|
||||
assertThat(WorkflowTabService.classify(cleared,"LOADED",pendingUnload))
|
||||
.contains("WAIT_UNLOAD").doesNotContain("WAIT_CUSTOMS_DOCS","EXPORT_CUSTOMS","SAILING_CLEARANCE");
|
||||
assertThat(WorkflowTabService.classify(cleared,"LOADED",unloaded))
|
||||
.doesNotContain("WAIT_UNLOAD","SAILING_CLEARANCE","EXPORT_CUSTOMS");
|
||||
assertThat(WorkflowTabService.classify(uploaded,"DELIVERED",unloaded))
|
||||
.contains("SIGNED").doesNotContain("EXPORT_CUSTOMS","WAIT_CUSTOMS_DOCS");
|
||||
}
|
||||
/** 构造独立分柜来源。 */
|
||||
private WorkflowFacts.Fact fact(long id,String kg,String confirmed,boolean uploaded) {
|
||||
return new WorkflowFacts.Fact(11,id,100+id,"APPROVED",new BigDecimal(kg),LocalDateTime.of(2026,10,19,10,0),true,uploaded,false,false,confirmed,null);
|
||||
}
|
||||
/** 有效建单移出待排车,取消后恢复;实际出库不计未出库单。 */
|
||||
@Test void cancellationRestoresDispatchQueue() {
|
||||
var f=fact(1,"100",null,true);
|
||||
var waiting=new WorkflowFacts.Overseas(11,1,false,false,false,true);
|
||||
var created=new WorkflowFacts.Overseas(11,1,true,true,false,true);
|
||||
var shipped=new WorkflowFacts.Overseas(11,1,true,false,true,true);
|
||||
assertThat(WorkflowTabService.classify(f,"LOADED",waiting)).contains("WAIT_DISPATCH").doesNotContain("OUTBOUND_DOCUMENT");
|
||||
assertThat(WorkflowTabService.classify(f,"LOADED",created)).contains("OUTBOUND_DOCUMENT").doesNotContain("WAIT_DISPATCH");
|
||||
assertThat(WorkflowTabService.classify(f,"LOADED",shipped)).contains("DELIVERING").doesNotContain("WAIT_DISPATCH","OUTBOUND_DOCUMENT");
|
||||
assertThat(WorkflowTabService.classify(f,"LOADED",waiting)).contains("WAIT_DISPATCH");
|
||||
}
|
||||
/** 一柜资料上传不覆盖另一柜;签收不再进入待排。 */
|
||||
@Test void independentSlicesAndUploadedClearance() {
|
||||
assertThat(WorkflowTabService.classify(fact(1,"100",null,false),"LOADED",new WorkflowFacts.Overseas(11,1,false,false,false,false)))
|
||||
.contains("SAILING_CLEARANCE","WAIT_DISPATCH").doesNotContain("WAIT_UNLOAD");
|
||||
assertThat(WorkflowTabService.classify(fact(2,"200",null,true),"LOADED",new WorkflowFacts.Overseas(11,2,true,true,false,false)))
|
||||
.contains("WAIT_UNLOAD","OUTBOUND_DOCUMENT").doesNotContain("SAILING_CLEARANCE","WAIT_DISPATCH");
|
||||
assertThat(WorkflowTabService.classify(fact(1,"100",null,false),"DELIVERED",null)).contains("SIGNED").doesNotContain("WAIT_DISPATCH");
|
||||
}
|
||||
/** 数值格式不改变版本,值或来源变化必须重新确认。 */
|
||||
@Test void independentWeightVersion() {
|
||||
var hash=ChargeableWeightService.fingerprint(fact(1,"100.0000",null,false));
|
||||
assertThat(ChargeableWeightService.weight(fact(1,"100",hash,false)).confirmed()).isTrue();
|
||||
assertThat(ChargeableWeightService.weight(fact(1,"101",hash,false)).confirmed()).isFalse();
|
||||
assertThat(ChargeableWeightService.weight(fact(2,"100",hash,false)).confirmed()).isFalse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.baiyue.firstleg.workflow;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.h2.jdbcx.JdbcDataSource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import java.nio.file.*;
|
||||
import java.util.List;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
/** 真实Mapper和隔离内存库验证资料归属、计费重确认和历史失效。 */
|
||||
class WorkflowFactPersistenceTest {
|
||||
/** 测试建表不连接业务数据库。 */
|
||||
@Test void clearanceAndWeightConfirmationPersistIndependently() throws Exception {
|
||||
var ds=new JdbcDataSource();ds.setURL("jdbc:h2:mem:weight-"+java.util.UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1");
|
||||
var db=new JdbcTemplate(ds);
|
||||
db.execute("CREATE TABLE fl_inbound_order(id BIGINT PRIMARY KEY,tenant_id BIGINT,order_id BIGINT,measure_review_status VARCHAR(30),chargeable_weight_kg DECIMAL(18,4),measure_completed_at TIMESTAMP,deleted INT,process_status VARCHAR(30))");
|
||||
db.execute("CREATE TABLE fl_loading_plan_item(id BIGINT,inbound_id BIGINT,account_id BIGINT,loading_plan_id BIGINT,deleted INT)");
|
||||
db.execute("CREATE TABLE fl_loading_plan(id BIGINT,plan_status VARCHAR(30),container_booking_id BIGINT)");
|
||||
db.execute("CREATE TABLE fl_loading_pick_task(id BIGINT,loading_plan_id BIGINT,task_status VARCHAR(30))");
|
||||
db.execute("CREATE TABLE fl_loading_pick_item(pick_task_id BIGINT,loading_plan_item_id BIGINT,item_status VARCHAR(30),picked_package_count INT)");
|
||||
db.execute("CREATE TABLE fl_container_booking(id BIGINT,record_status VARCHAR(30))");
|
||||
db.execute("CREATE TABLE fl_carrier_schedule_node_fact(container_booking_id BIGINT,standard_node VARCHAR(40),actual_at TIMESTAMP)");
|
||||
db.execute("CREATE TABLE fl_container_document(container_booking_id BIGINT,document_type VARCHAR(40),file_id BIGINT)");
|
||||
db.execute("CREATE TABLE fl_customs_customer_document_inbound(inbound_id BIGINT,tenant_id BIGINT,document_id BIGINT)");
|
||||
db.execute("CREATE TABLE fl_customs_customer_document(id BIGINT,tenant_id BIGINT,status VARCHAR(30),customs_container_id BIGINT)");
|
||||
db.execute("CREATE TABLE fl_customs_container(id BIGINT,loading_plan_id BIGINT,status VARCHAR(40))");
|
||||
db.execute("ALTER TABLE fl_inbound_order ADD tax_mode VARCHAR(30)");
|
||||
db.execute("ALTER TABLE fl_inbound_order ADD parent_inbound_id BIGINT");
|
||||
db.execute("ALTER TABLE fl_inbound_order ADD inbound_role VARCHAR(30)");
|
||||
db.execute("ALTER TABLE fl_inbound_order ADD split_purpose VARCHAR(30)");
|
||||
db.execute(Files.readString(Path.of("src/main/resources/db/chargeable-weight-confirmation.sql")));
|
||||
db.update("INSERT INTO fl_inbound_order(id,tenant_id,order_id,measure_review_status,chargeable_weight_kg,measure_completed_at,deleted,process_status,tax_mode) VALUES(1,11,101,'APPROVED',100,CURRENT_TIMESTAMP,0,'LOADED','TAX_REFUND'),(2,22,102,'APPROVED',200,CURRENT_TIMESTAMP,0,'LOADED','BUY_DECLARATION')");
|
||||
db.update("INSERT INTO fl_loading_plan_item VALUES(3,1,11,4,0)");
|
||||
db.update("INSERT INTO fl_loading_plan VALUES(4,'COMPLETED',5)");
|
||||
db.update("INSERT INTO fl_container_booking VALUES(5,'ACTIVE')");
|
||||
db.update("INSERT INTO fl_loading_pick_task VALUES(6,4,'COMPLETED')");
|
||||
db.update("INSERT INTO fl_loading_pick_item VALUES(6,3,'PICKED',2)");
|
||||
db.update("INSERT INTO fl_carrier_schedule_node_fact VALUES(5,'DEPARTED_ORIGIN_PORT',CURRENT_TIMESTAMP)");
|
||||
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.addMapper(WorkflowFactMapper.class);
|
||||
config.addMapper(com.baiyue.firstleg.mapper.ContainerBookingMapper.class);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);
|
||||
var mapper=new SqlSessionTemplate(factory.getObject()).getMapper(WorkflowFactMapper.class);
|
||||
var containers=new SqlSessionTemplate(factory.getObject()).getMapper(com.baiyue.firstleg.mapper.ContainerBookingMapper.class);
|
||||
assertThat(containers.hasActualVesselDeparture(5)).isOne();
|
||||
assertThat(containers.hasActualVesselDeparture(6)).isZero();
|
||||
db.update("INSERT INTO fl_carrier_schedule_node_fact VALUES(6,'DEPARTED_ORIGIN_PORT',NULL),(6,'ARRIVED_DESTINATION_PORT',CURRENT_TIMESTAMP)");
|
||||
assertThat(containers.hasActualVesselDeparture(6)).isZero();
|
||||
var repository=new WorkflowFactRepository(mapper);var service=new ChargeableWeightService(repository);
|
||||
assertThat(service.read(22,101)).isEmpty();
|
||||
assertThat(mapper.facts(List.of(1L))).singleElement().satisfies(f->{assertThat(f.departed()).isTrue();assertThat(f.clearanceUploaded()).isFalse();assertThat(f.missingCustoms()).isTrue();});
|
||||
db.update("INSERT INTO fl_container_document VALUES(5,'DESTINATION_CLEARANCE',99)");
|
||||
assertThat(mapper.facts(List.of(1L)).get(0).clearanceUploaded()).isTrue();
|
||||
db.update("INSERT INTO fl_customs_container VALUES(8,4,'WAITING_CUSTOMER_DOCUMENTS')");
|
||||
db.update("INSERT INTO fl_customs_customer_document VALUES(9,22,'ACTIVE',8)");
|
||||
db.update("INSERT INTO fl_customs_customer_document_inbound VALUES(1,22,9)");
|
||||
assertThat(mapper.facts(List.of(1L))).singleElement().satisfies(f->{
|
||||
assertThat(f.missingCustoms()).isTrue();assertThat(f.exportCustomsReady()).isFalse();
|
||||
});
|
||||
db.update("INSERT INTO fl_customs_customer_document VALUES(10,11,'ACTIVE',8)");
|
||||
db.update("INSERT INTO fl_customs_customer_document_inbound VALUES(1,11,10)");
|
||||
assertThat(mapper.facts(List.of(1L))).singleElement().satisfies(f->{assertThat(f.missingCustoms()).isFalse();assertThat(f.exportCustomsReady()).isTrue();});
|
||||
// 本票上传后即可汇总,不必等同柜其他票或点击开始报关;完成报关后仍等待实际开船。
|
||||
for (String status : List.of("READY_TO_START", "DECLARING", "COMPLETED")) {
|
||||
db.update("UPDATE fl_customs_container SET status=? WHERE id=8", status);
|
||||
assertThat(mapper.facts(List.of(1L)).get(0).exportCustomsReady()).isTrue();
|
||||
}
|
||||
db.update("UPDATE fl_customs_customer_document SET status='DELETED' WHERE id=10");
|
||||
assertThat(mapper.facts(List.of(1L))).singleElement().satisfies(f->{
|
||||
assertThat(f.missingCustoms()).isTrue();assertThat(f.exportCustomsReady()).isFalse();
|
||||
});
|
||||
db.update("UPDATE fl_customs_customer_document SET status='ACTIVE' WHERE id=10");
|
||||
db.update("INSERT INTO fl_customs_container VALUES(18,14,'READY_TO_START')");
|
||||
db.update("UPDATE fl_customs_customer_document SET customs_container_id=18 WHERE id=10");
|
||||
assertThat(mapper.facts(List.of(1L)).get(0).exportCustomsReady()).isFalse();
|
||||
db.update("UPDATE fl_customs_customer_document SET customs_container_id=8 WHERE id=10");
|
||||
db.update("UPDATE fl_loading_plan SET plan_status='LOADING' WHERE id=4");
|
||||
assertThat(mapper.facts(List.of(1L)).get(0).exportCustomsReady()).isFalse();
|
||||
db.update("UPDATE fl_loading_plan SET plan_status='COMPLETED' WHERE id=4");
|
||||
db.update("UPDATE fl_loading_pick_item SET picked_package_count=0");
|
||||
assertThat(mapper.facts(List.of(1L)).get(0).exportCustomsReady()).isFalse();
|
||||
db.update("UPDATE fl_loading_pick_item SET picked_package_count=2");
|
||||
// 不把预计开船当成实际开船;手动或拉取写入的实际节点都读同一事实表。
|
||||
db.update("UPDATE fl_carrier_schedule_node_fact SET actual_at=NULL");
|
||||
assertThat(mapper.facts(List.of(1L)).get(0).departed()).isFalse();
|
||||
db.update("UPDATE fl_carrier_schedule_node_fact SET actual_at=CURRENT_TIMESTAMP");
|
||||
assertThat(mapper.facts(List.of(1L)).get(0).departed()).isTrue();
|
||||
// 买单票无需客户上传退税资料,但同样必须有实际装柜证据。
|
||||
assertThat(mapper.facts(List.of(2L)).get(0).exportCustomsReady()).isFalse();
|
||||
db.update("INSERT INTO fl_loading_plan_item VALUES(13,2,22,4,0)");
|
||||
db.update("INSERT INTO fl_loading_pick_item VALUES(6,13,'PICKED',1)");
|
||||
assertThat(mapper.facts(List.of(2L))).singleElement().satisfies(f->{
|
||||
assertThat(f.missingCustoms()).isFalse();assertThat(f.exportCustomsReady()).isTrue();
|
||||
});
|
||||
var current=service.read(11,101).get(0);
|
||||
assertThat(service.confirm(11,7,1,"cmd",new WorkflowFacts.Confirm(current.fingerprint())).confirmed()).isTrue();
|
||||
assertThat(service.confirm(11,7,1,"cmd",new WorkflowFacts.Confirm(current.fingerprint())).confirmed()).isTrue();
|
||||
assertThat(db.queryForObject("SELECT COUNT(*) FROM fl_chargeable_weight_confirmation",Integer.class)).isEqualTo(1);
|
||||
assertThatThrownBy(()->service.confirm(11,7,1,"cmd",new WorkflowFacts.Confirm("0".repeat(64)))).isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
||||
db.update("UPDATE fl_inbound_order SET chargeable_weight_kg=101 WHERE id=1");
|
||||
assertThat(service.read(11,101).get(0).confirmed()).isFalse();
|
||||
assertThatThrownBy(()->service.confirm(11,7,1,"old",new WorkflowFacts.Confirm(current.fingerprint()))).isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
||||
assertThatThrownBy(()->service.confirm(22,7,1,"foreign",new WorkflowFacts.Confirm(current.fingerprint()))).isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
||||
// 现行部分装柜由来源单承接实装,余量子单独立继续分柜,两票均应保留。
|
||||
db.update("INSERT INTO fl_inbound_order(id,tenant_id,order_id,measure_review_status,chargeable_weight_kg,measure_completed_at,deleted,process_status,tax_mode,parent_inbound_id,inbound_role,split_purpose) VALUES(3,11,103,'APPROVED',50,CURRENT_TIMESTAMP,0,'WAIT_SPLIT','BUY_DECLARATION',1,'CHILD','REMAINDER')");
|
||||
assertThat(mapper.facts(List.of(1L,3L))).hasSize(2);
|
||||
assertThat(mapper.facts(List.of(1L)).get(0).departed()).isTrue();
|
||||
assertThat(mapper.facts(List.of(3L)).get(0).departed()).isFalse();
|
||||
assertThat(service.read(11,101)).hasSize(1);
|
||||
assertThat(service.read(11,103)).hasSize(1);
|
||||
// 旧模型已将实装移至独立子票时,父汇总不能重复进入页签或确认。
|
||||
db.update("UPDATE fl_inbound_order SET process_status='LOADED',split_purpose='LOADED' WHERE id=3");
|
||||
assertThat(service.read(11,101)).isEmpty();
|
||||
assertThatThrownBy(()->service.confirm(11,7,1,"parent",new WorkflowFacts.Confirm(current.fingerprint()))).isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
||||
var child=service.read(11,103).get(0);
|
||||
assertThat(child.confirmed()).isFalse();
|
||||
assertThatThrownBy(()->service.confirm(11,7,3,"cmd",new WorkflowFacts.Confirm(child.fingerprint()))).isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
||||
assertThat(service.confirm(11,7,3,"child",new WorkflowFacts.Confirm(child.fingerprint())).confirmed()).isTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user