fix(customs): add broker submission and receipt action history

This commit is contained in:
2026-09-28 13:21:37 +08:00
parent ab64699245
commit e63cb2b29e
15 changed files with 365 additions and 8 deletions
@@ -17,7 +17,9 @@ class ContainerCustomsControllerContractTest {
.containsExactly("/api/platform/v1/customs/loading-plans");
assertPermission(PlatformContainerCustomsController.class,"detail","platform:firstleg:customs:list");
assertPermission(PlatformContainerCustomsController.class,"start","platform:firstleg:customs:manage");
assertPermission(PlatformContainerCustomsController.class,"sendInitialToBroker","platform:firstleg:customs:manage");
assertPermission(PlatformContainerCustomsController.class,"uploadCompletion","platform:firstleg:customs:manage");
assertPermission(PlatformContainerCustomsController.class,"receiptActions","platform:firstleg:customs:list");
assertPermission(PlatformContainerCustomsController.class,"completeAllBuy","platform:firstleg:customs:manage");
assertPermission(PlatformContainerCustomsController.class,"declarationBatches","platform:firstleg:customs:list");
assertPermission(PlatformContainerCustomsController.class,"createDeclarationBatch","platform:firstleg:customs:manage");
@@ -34,12 +36,13 @@ class ContainerCustomsControllerContractTest {
assertPermission(MerchantContainerCustomsController.class,"scope","merchant:order:edit");
assertPermission(MerchantContainerCustomsController.class,"delete","merchant:order:edit");
assertPermission(MerchantContainerCustomsController.class,"clearanceResults","merchant:order:view");
assertPermission(MerchantContainerCustomsController.class,"receiptActions","merchant:order:view");
}
@Test void openApiPublishesEveryPathParameterAndGeneratableDtos()throws Exception{
String yaml=new ClassPathResource("openapi/customs-v1.yaml").getContentAsString(java.nio.charset.StandardCharsets.UTF_8);
new Yaml().load(yaml);
assertThat(yaml).contains("ContainerView:","DocumentView:","FileView:","InboundView:","ActionRequest:","ScopeRequest:",
"DeclarationBatch:","ClearanceResult:","declaration-batches","clearance-results")
"DeclarationBatch:","BrokerSubmissionView:","ClearanceResult:","ReceiptActionView:","confirmationHistory","brokerSubmission","send-to-broker","declaration-batches","clearance-results")
.contains("merchantStatus:","WAITING_UPLOAD","CUSTOMS_COMPLETED","loadingPlanId:","hasActualCargo:","nullable: true")
.contains("LOADING_LIST_BUY_DECLARATION_ITEMS")
.doesNotContain("/cases/","CUSTOMS_DECLARATION");
@@ -46,6 +46,7 @@ class ContainerCustomsMapperContractTest {
assertRecordProjection("completion", ContainerCustomsMapper.CompletionRow.class, long.class);
assertRecordProjection("lockCompletion", ContainerCustomsMapper.CompletionRow.class, long.class);
assertRecordProjectionWithoutVersion("completionVersions", ContainerCustomsMapper.CompletionVersionRow.class, long.class);
assertRecordProjectionWithoutVersion("receiptActionHistory", ContainerCustomsMapper.ReceiptActionRow.class, long.class, Long.class);
}
@Test void declarationAndClearanceQueriesMapExplicitHistoricalFacts() throws Exception {
@@ -57,6 +58,11 @@ class ContainerCustomsMapperContractTest {
assertRecordProjectionWithoutVersion("lockClearanceResult", ContainerCustomsMapper.ClearanceResultRow.class, long.class);
}
@Test void brokerSubmissionQueriesMapIndependentFirstSendFact() throws Exception {
assertRecordProjection("brokerSubmission", ContainerCustomsMapper.BrokerSubmissionRow.class, long.class);
assertRecordProjection("lockBrokerSubmission", ContainerCustomsMapper.BrokerSubmissionRow.class, long.class);
}
private static void assertRecordProjection(String method,Class<?> recordType,Class<?>... parameterTypes) throws Exception {
assertProjection(method, recordType, true, parameterTypes);
}
@@ -150,6 +150,61 @@ class ContainerCustomsServiceTest {
verify(mapper,never()).freezeScopes(anyLong());
}
@Test void initialBrokerSendCreatesIndependentFrozenSnapshotBatch() throws Exception {
var inbound=new ContainerCustomsMapper.InboundRow(9L,"LP9",8L,"COMPLETED","CAB","C",11L,21L,"O","I",7L,"客户","TAX_REFUND");
var declaring=container(ContainerCustomsService.DECLARING,3);
var source=new ContainerCustomsMapper.DocumentRow(31L,2L,7L,10L,"source.bin","a".repeat(64),
"application/octet-stream",1L,"ACTIVE",5L,java.time.LocalDateTime.now(),1,java.time.LocalDateTime.now(),1);
var submission=new ContainerCustomsMapper.BrokerSubmissionRow(41L,2L,9L,"BR-LP9-01","SENT",3,"admin",
java.time.LocalDateTime.now(),1);
when(mapper.claimCommand(anyLong(),eq(5L),eq("SEND_CUSTOMS_INITIAL_TO_BROKER_9"),eq("send-key"),anyString())).thenReturn(1);
when(mapper.lockContainer(9L)).thenReturn(declaring);
when(mapper.taxRefundCount(9L)).thenReturn(1);
when(mapper.brokerSubmission(2L)).thenReturn(null,submission);
when(mapper.documents(2L,null)).thenReturn(List.of(source));
when(mapper.frozenScopes(31L)).thenReturn(List.of(new ContainerCustomsMapper.ScopeRow(31L,11L,21L,"I")));
when(objectMapper.writeValueAsString(any())).thenReturn("[]");
when(mapper.insertBrokerSubmission(anyLong(),eq(2L),eq(9L),eq("BR-LP9-01"),eq(3),eq("[]"),eq(5L),eq("admin"))).thenReturn(1);
when(mapper.bumpVersionAfterBrokerSubmission(2L,3)).thenReturn(1);
when(mapper.insertHistory(anyLong(),eq(2L),eq("SEND_CUSTOMS_INITIAL_TO_BROKER"),eq(ContainerCustomsService.DECLARING),eq(ContainerCustomsService.DECLARING),isNull(),eq(5L),eq("admin"))).thenReturn(1);
when(mapper.completeCommand(eq(5L),eq("SEND_CUSTOMS_INITIAL_TO_BROKER_9"),eq("send-key"),anyLong())).thenReturn(1);
when(mapper.planInbounds(9L)).thenReturn(List.of(inbound));
when(mapper.plan(9L)).thenReturn(new ContainerCustomsMapper.PlanRow(9L,"LP9",8L,"COMPLETED","CAB","C"));
when(mapper.container(9L)).thenReturn(declaring);
when(mapper.actualBoxCount(9L,null,null)).thenReturn(0L);
when(mapper.finalDocument(31L)).thenReturn(null);
when(mapper.completion(31L)).thenReturn(null);
var result=service.sendInitialToBroker(9L,5L,"admin","send-key",new ActionRequest(3,true,null));
assertThat(result.brokerSubmission()).isNotNull();
assertThat(result.brokerSubmission().batchNo()).isEqualTo("BR-LP9-01");
assertThat(result.brokerSubmission().sourceVersion()).isEqualTo(3);
verify(mapper).insertBrokerSubmission(anyLong(),eq(2L),eq(9L),eq("BR-LP9-01"),eq(3),eq("[]"),eq(5L),eq("admin"));
}
@Test void initialBrokerSendRejectsStaleAggregateVersion(){
var declaring=container(ContainerCustomsService.DECLARING,4);
when(mapper.claimCommand(anyLong(),eq(5L),eq("SEND_CUSTOMS_INITIAL_TO_BROKER_9"),eq("stale-key"),anyString())).thenReturn(1);
when(mapper.lockContainer(9L)).thenReturn(declaring);
assertThatThrownBy(()->service.sendInitialToBroker(9L,5L,"admin","stale-key",new ActionRequest(3,true,null)))
.isInstanceOf(BizException.class).hasMessageContaining("其他人修改");
verify(mapper,never()).insertBrokerSubmission(anyLong(),anyLong(),anyLong(),anyString(),anyInt(),anyString(),anyLong(),anyString());
}
@Test void receiptUploadRejectsWhenInitialBrokerSubmissionIsMissing(){
var source=document(1L);var container=container(ContainerCustomsService.DECLARING,3);
when(mapper.claimCommand(anyLong(),eq(5L),eq("UPLOAD_COMPLETION_1"),eq("before-send"),anyString())).thenReturn(1);
when(mapper.document(1L)).thenReturn(source);when(mapper.lockContainerById(2L)).thenReturn(container);
when(mapper.lockDocument(1L)).thenReturn(source);when(mapper.brokerSubmission(2L)).thenReturn(null);
assertThatThrownBy(()->service.uploadCompletion(1L,5L,"admin","before-send",0,
new MockMultipartFile("file","receipt.pdf","application/pdf",new byte[]{1})))
.isInstanceOf(BizException.class).hasMessageContaining("首次发送报关行资料");
verifyNoInteractions(files);
}
@Test void declaringContainerFreezesUploadReplacementScopeAndDeletionWithoutDrift(){
var inbound=new ContainerCustomsMapper.InboundRow(9L,"LP9",8L,"COMPLETED","CAB","C",11L,21L,"O","I",7L,"客户","TAX_REFUND");
var declaring=container(ContainerCustomsService.DECLARING,3);
@@ -247,6 +302,7 @@ class ContainerCustomsServiceTest {
var source=document(1L);var container=container(ContainerCustomsService.DECLARING,3);
when(mapper.claimCommand(anyLong(),eq(5L),eq("UPLOAD_COMPLETION_1"),eq("k"),anyString())).thenReturn(1);
when(mapper.document(1L)).thenReturn(source);when(mapper.lockContainerById(2L)).thenReturn(container);
when(mapper.brokerSubmission(2L)).thenReturn(brokerSubmission());
when(mapper.lockDocument(1L)).thenReturn(source);when(mapper.lockCompletion(1L)).thenReturn(null);
when(files.upload(any(),eq(5L),eq(7L),eq("LOADING_LIST_CUSTOMS_RECEIPT"),eq("9"),any()))
.thenReturn(com.baiyue.common.core.api.ApiResponse.success(new CustomsFileClient.Metadata(99L,7L,
@@ -307,6 +363,7 @@ class ContainerCustomsServiceTest {
when(mapper.command(5L,"UPLOAD_COMPLETION_1","completion-key")).thenAnswer(invocation->commandState.get());
when(mapper.lockContainerById(2L)).thenAnswer(invocation->containerState.get());
when(mapper.lockDocument(1L)).thenReturn(source);
when(mapper.brokerSubmission(2L)).thenReturn(brokerSubmission());
when(mapper.lockCompletion(1L)).thenAnswer(invocation->completionState.get());
when(files.upload(any(),eq(5L),eq(7L),eq("LOADING_LIST_CUSTOMS_RECEIPT"),eq("9"),same(file)))
.thenReturn(com.baiyue.common.core.api.ApiResponse.success(metadata));
@@ -382,6 +439,7 @@ class ContainerCustomsServiceTest {
}
private static ContainerCustomsMapper.DocumentRow document(long id){return new ContainerCustomsMapper.DocumentRow(id,2L,7L,10L,"source.bin","a".repeat(64),"application/octet-stream",1L,"ACTIVE",5L,java.time.LocalDateTime.now(),1,java.time.LocalDateTime.now(),1);}
private static ContainerCustomsMapper.BrokerSubmissionRow brokerSubmission(){return new ContainerCustomsMapper.BrokerSubmissionRow(41L,2L,9L,"BR-LP9-01","SENT",3,"admin",java.time.LocalDateTime.now(),1);}
private static ContainerCustomsMapper.ContainerRow container(String status,int version){return new ContainerCustomsMapper.ContainerRow(2L,9L,8L,status,null,null,null,null,null,null,null,null,version);}
private void assertMerchantOrderNotFound(long tenantId,long orderId){
assertThatThrownBy(()->service.merchantOrderView(tenantId,orderId))