feat(firstleg): implement container customs workflow
This commit is contained in:
+1
-1
@@ -42,7 +42,7 @@ class ContainerControllerContractTest {
|
||||
assertThat(openapi).contains("/api/platform/v1/containers/{id}/overview:",
|
||||
"/documents/{documentType}/{fileId}/download:", "ExceptionRequest",
|
||||
"/clearance/document/{fileId}/download:",
|
||||
"DESTINATION_CLEARANCE、CUSTOMS_DECLARATION、DELIVERY_ORDER、PICKUP_ORDER",
|
||||
"DESTINATION_CLEARANCE、DELIVERY_ORDER、PICKUP_ORDER",
|
||||
"enum: [DELIVERY_ORDER, PICKUP_ORDER]", "Idempotency-Key", "photoCounts:",
|
||||
"/pda/photos/{fileId}/download:", "downloadPath:", "workflowStatus:",
|
||||
"AWAITING_REVIEW", "version: {type: integer, minimum: 0}",
|
||||
|
||||
+2
-53
@@ -6,19 +6,12 @@ import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmPreloadRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentAttachRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentCompleteRequest;
|
||||
import com.baiyue.firstleg.service.LoadingListService;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
@@ -42,15 +35,6 @@ class LoadingListControllerContractTest {
|
||||
authority("confirmedPage", "platform:firstleg:loading-list:list", AuthenticatedUser.class,
|
||||
ConfirmedQuery.class);
|
||||
authority("confirmedDetail", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
|
||||
authority("declaration", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
|
||||
authority("downloadDeclaration", "platform:firstleg:loading-list:list", AuthenticatedUser.class,
|
||||
long.class, long.class, long.class);
|
||||
authority("attachDeclaration", "platform:firstleg:customs:manage", AuthenticatedUser.class,
|
||||
long.class, long.class, String.class, DocumentAttachRequest.class);
|
||||
authority("uploadDeclaration", "platform:firstleg:customs:manage", AuthenticatedUser.class,
|
||||
long.class, long.class, String.class, MultipartFile.class, int.class, String.class);
|
||||
authority("completeDeclaration", "platform:firstleg:customs:manage", AuthenticatedUser.class,
|
||||
long.class, long.class, String.class, DocumentCompleteRequest.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -92,8 +76,6 @@ class LoadingListControllerContractTest {
|
||||
.contains("oceanBookingStatus")
|
||||
.contains("pdaTaskId")
|
||||
.contains("cabinetNo")
|
||||
.contains("LOADING_CUSTOMS_DECLARATION")
|
||||
.contains("Cache-Control 为 no-store")
|
||||
.doesNotContain("internalContainerNo")
|
||||
.doesNotContain("rollback")
|
||||
.doesNotContain("delete:")
|
||||
@@ -112,30 +94,6 @@ class LoadingListControllerContractTest {
|
||||
assertThat(component(ConfirmedQuery.class, "warehouseId").getAnnotation(NotNull.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipartScalarFieldsUseRequestParameters() throws Exception {
|
||||
Method method = PlatformLoadingListController.class.getDeclaredMethod("uploadDeclaration",
|
||||
AuthenticatedUser.class, long.class, long.class, String.class,
|
||||
MultipartFile.class, int.class, String.class);
|
||||
assertThat(method.getParameters()[4].getAnnotation(RequestPart.class)).isNotNull();
|
||||
assertThat(method.getParameters()[5].getAnnotation(RequestParam.class)).isNotNull();
|
||||
assertThat(method.getParameters()[5].getAnnotation(RequestPart.class)).isNull();
|
||||
assertThat(method.getParameters()[6].getAnnotation(RequestParam.class)).isNotNull();
|
||||
assertThat(method.getParameters()[6].getAnnotation(RequestPart.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customsDownloadUsesStablePlanPathAndBinaryResponse() throws Exception {
|
||||
Method method = PlatformLoadingListController.class.getDeclaredMethod("downloadDeclaration",
|
||||
AuthenticatedUser.class, long.class, long.class, long.class);
|
||||
assertThat(method.getAnnotation(GetMapping.class).value()).containsExactly(
|
||||
"/loading-lists/{planId}/customs/document/{fileId}/download");
|
||||
assertThat(method.getReturnType()).isEqualTo(ResponseEntity.class);
|
||||
assertThat(method.getParameters()[1].getName()).isEqualTo("planId");
|
||||
assertThat(method.getParameters()[2].getName()).isEqualTo("fileId");
|
||||
assertThat(method.getParameters()[3].getName()).isEqualTo("warehouseId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void yamlParses() throws Exception {
|
||||
String yaml = new ClassPathResource("openapi/loading-lists-v1.yaml")
|
||||
@@ -149,23 +107,14 @@ class LoadingListControllerContractTest {
|
||||
"/api/platform/v1/first-leg/loading-plans/{planId}/transport",
|
||||
"/api/platform/v1/first-leg/loading-plans/{planId}/confirm",
|
||||
"/api/platform/v1/first-leg/loading-lists",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}/customs",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/document",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/document/{fileId}/download",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/document/upload",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/complete");
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}");
|
||||
Map<String, Object> components = stringMap(document.get("components"));
|
||||
Map<String, Object> schemas = stringMap(components.get("schemas"));
|
||||
assertThat(schemas).containsKeys(
|
||||
"LongId", "PreloadPage", "ConfirmedPage", "Customer", "PreloadSummary",
|
||||
"ConfirmedSummary", "LoadingItem", "OperationHistory", "PreloadDetail",
|
||||
"ConfirmedDetail", "TransportUpdateRequest", "PreloadReadiness",
|
||||
"ConfirmPreloadRequest", "PreloadConfirmation", "PreloadConfirmationEnvelope",
|
||||
"BusinessDocumentEnvelope",
|
||||
"DocumentAttachRequest", "DocumentUploadRequest", "DocumentCompleteRequest", "BusinessDocument");
|
||||
assertThat(properties(schemas, "DocumentUploadRequest"))
|
||||
.containsOnlyKeys("file", "expectedVersion", "operationReason");
|
||||
"ConfirmPreloadRequest", "PreloadConfirmation", "PreloadConfirmationEnvelope");
|
||||
assertThat(paths)
|
||||
.doesNotContainKey("/api/platform/v1/first-leg/loading-plans/{planId}/complete");
|
||||
assertThat(stringMap(schemas.get("LongId"))).containsEntry("type", "string");
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.baiyue.firstleg.customs.container;
|
||||
|
||||
import com.baiyue.firstleg.customs.container.controller.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ContainerCustomsControllerContractTest {
|
||||
@Test void platformUsesListForReadsAndManageForWrites() {
|
||||
assertThat(PlatformContainerCustomsController.class.getAnnotation(RequestMapping.class).value())
|
||||
.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,"uploadCompletion","platform:firstleg:customs:manage");
|
||||
assertPermission(PlatformContainerCustomsController.class,"completeAllBuy","platform:firstleg:customs:manage");
|
||||
}
|
||||
@Test void merchantUsesOrderViewAndEditWithoutTrustingTenantInput() {
|
||||
assertThat(MerchantContainerCustomsController.class.getAnnotation(RequestMapping.class).value())
|
||||
.containsExactly("/api/merchant/v1/customs");
|
||||
assertPermission(MerchantContainerCustomsController.class,"order","merchant:order:view");
|
||||
assertPermission(MerchantContainerCustomsController.class,"upload","merchant:order:edit");
|
||||
assertPermission(MerchantContainerCustomsController.class,"replace","merchant:order:edit");
|
||||
assertPermission(MerchantContainerCustomsController.class,"scope","merchant:order:edit");
|
||||
assertPermission(MerchantContainerCustomsController.class,"delete","merchant:order:edit");
|
||||
}
|
||||
@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:")
|
||||
.contains("merchantStatus:","WAITING_UPLOAD","CUSTOMS_COMPLETED","loadingPlanId:")
|
||||
.contains("LOADING_LIST_BUY_DECLARATION_ITEMS")
|
||||
.doesNotContain("/cases/","CUSTOMS_DECLARATION");
|
||||
}
|
||||
private static void assertPermission(Class<?> type,String method,String permission) {
|
||||
Method value=java.util.Arrays.stream(type.getDeclaredMethods()).filter(m->m.getName().equals(method)).findFirst().orElseThrow();
|
||||
assertThat(value.getAnnotation(PreAuthorize.class).value()).contains(permission);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.firstleg.customs.container;
|
||||
|
||||
import com.baiyue.firstleg.customs.container.mapper.ContainerCustomsMapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ContainerCustomsMapperContractTest {
|
||||
@Test void actualCompletedLoadingItemsAndTaxModeAreTheOnlyTicketFacts() throws Exception {
|
||||
String plan=sql("planInbounds",long.class);
|
||||
assertThat(plan).contains("fl_loading_plan_item","fl_inbound_order","p.id=#{planId}","item.deleted=0");
|
||||
String missing=sql("missingRefundCount",long.class,long.class);
|
||||
assertThat(missing).contains("i.tax_mode='TAX_REFUND'","fl_customs_customer_document_inbound","d.status='ACTIVE'");
|
||||
}
|
||||
@Test void buyExportIsCompletedPlanActualItemsOnly() throws Exception {
|
||||
String value=sql("buyRows",long.class,long.class,long.class,int.class);
|
||||
assertThat(value).contains("p.plan_status='COMPLETED'","i.tax_mode='BUY_DECLARATION'","item.deleted=0")
|
||||
.contains("item.id>#{cursor}","item.id<=#{maximumId}");
|
||||
}
|
||||
private static String sql(String name,Class<?>... types) throws Exception {
|
||||
return String.join(" ",ContainerCustomsMapper.class.getMethod(name,types).getAnnotation(Select.class).value());
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.baiyue.firstleg.customs.container;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.customs.container.dto.ContainerCustomsDtos.ActionRequest;
|
||||
import com.baiyue.firstleg.customs.container.mapper.ContainerCustomsMapper;
|
||||
import com.baiyue.firstleg.customs.container.service.ContainerCustomsService;
|
||||
import com.baiyue.firstleg.customs.client.CustomsFileClient;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.mockito.*;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ContainerCustomsServiceTest {
|
||||
@Mock ContainerCustomsMapper mapper;
|
||||
@Mock CustomsFileClient files;
|
||||
@Mock ObjectMapper objectMapper;
|
||||
@Mock ApplicationEventPublisher events;
|
||||
@InjectMocks ContainerCustomsService service;
|
||||
@BeforeEach void setUp(){MockitoAnnotations.openMocks(this);}
|
||||
|
||||
@Test void startFailsClosedWhenAnyRefundInboundHasNoActiveDocumentCoverage(){
|
||||
var row=new ContainerCustomsMapper.InboundRow(9L,"LP9",8L,"COMPLETED","CAB","C",11L,21L,"O","I",7L,"客户","TAX_REFUND");
|
||||
var container=new ContainerCustomsMapper.ContainerRow(1L,9L,8L,ContainerCustomsService.READY,null,null,null,null,null,null,null,null,2);
|
||||
when(mapper.planInbounds(9L)).thenReturn(List.of(row));when(mapper.container(9L)).thenReturn(container);
|
||||
when(mapper.missingRefundCount(9L,1L)).thenReturn(0,1);when(mapper.claimCommand(anyLong(),eq(5L),anyString(),eq("k"),anyString())).thenReturn(1);
|
||||
when(mapper.lockContainer(9L)).thenReturn(container);
|
||||
|
||||
assertThatThrownBy(()->service.start(9L,5L,"admin","k",new ActionRequest(2,true,null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("退税入仓码未上传");
|
||||
|
||||
verify(mapper,never()).freezeScopes(anyLong());verify(mapper,never()).freezeDocuments(anyLong());verify(mapper,never()).start(anyLong(),anyInt(),anyLong(),anyString());
|
||||
}
|
||||
|
||||
@Test void startRejectsStaleContainerVersionBeforeAnyFreeze(){
|
||||
var row=new ContainerCustomsMapper.InboundRow(9L,"LP9",8L,"COMPLETED","CAB","C",11L,21L,"O","I",7L,"客户","TAX_REFUND");
|
||||
var container=new ContainerCustomsMapper.ContainerRow(1L,9L,8L,ContainerCustomsService.READY,null,null,null,null,null,null,null,null,3);
|
||||
when(mapper.planInbounds(9L)).thenReturn(List.of(row));when(mapper.container(9L)).thenReturn(container);
|
||||
when(mapper.missingRefundCount(9L,1L)).thenReturn(0);when(mapper.claimCommand(anyLong(),eq(5L),anyString(),eq("k"),anyString())).thenReturn(1);
|
||||
when(mapper.lockContainer(9L)).thenReturn(container);
|
||||
assertThatThrownBy(()->service.start(9L,5L,"admin","k",new ActionRequest(2,true,null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("其他人修改");
|
||||
verify(mapper,never()).freezeScopes(anyLong());
|
||||
}
|
||||
|
||||
@Test void lastCompletionRequiresConfirmationBeforeCallingFile(){
|
||||
var source=document(1L);var container=container(ContainerCustomsService.DECLARING,3);
|
||||
when(mapper.document(1L)).thenReturn(source);when(mapper.lockContainerById(2L)).thenReturn(container);
|
||||
when(mapper.lockDocument(1L)).thenReturn(source);when(mapper.lockCompletion(1L)).thenReturn(null);
|
||||
when(mapper.missingCompletionCount(2L)).thenReturn(1);
|
||||
var file=new MockMultipartFile("file","result.bin","application/octet-stream",new byte[]{1});
|
||||
assertThatThrownBy(()->service.uploadCompletion(1L,5L,"admin","k",0,false,file))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("最后一份");
|
||||
verifyNoInteractions(files);
|
||||
}
|
||||
|
||||
@Test void completedContainerLocksCompletionReplacement(){
|
||||
var source=document(1L);when(mapper.document(1L)).thenReturn(source);
|
||||
when(mapper.lockContainerById(2L)).thenReturn(container(ContainerCustomsService.COMPLETED,4));
|
||||
when(mapper.lockDocument(1L)).thenReturn(source);
|
||||
var file=new MockMultipartFile("file","result.bin","application/octet-stream",new byte[]{1});
|
||||
assertThatThrownBy(()->service.uploadCompletion(1L,5L,"admin","k",0,false,file))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不在报关中");
|
||||
verifyNoInteractions(files);
|
||||
}
|
||||
|
||||
@Test void merchantCannotDownloadCompletionBeforeWholeContainerCompletes(){
|
||||
var source=document(1L);when(mapper.document(1L)).thenReturn(source);
|
||||
when(mapper.containerById(2L)).thenReturn(container(ContainerCustomsService.DECLARING,3));
|
||||
assertThatThrownBy(()->service.downloadCompletion(7L,1L,false)).isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("完成文件不存在");
|
||||
verifyNoInteractions(files);
|
||||
}
|
||||
|
||||
@Test void allBuyCompletionRejectsUnknownOrMixedTaxMode(){
|
||||
var value=container(ContainerCustomsService.DECLARING,3);when(mapper.lockContainer(9L)).thenReturn(value);
|
||||
when(mapper.claimCommand(anyLong(),eq(5L),anyString(),eq("k"),anyString())).thenReturn(1);
|
||||
when(mapper.planInbounds(9L)).thenReturn(List.of(new ContainerCustomsMapper.InboundRow(9L,"LP",8L,"COMPLETED","CAB","C",11L,21L,"O","I",7L,"客户",null)));
|
||||
assertThatThrownBy(()->service.completeAllBuy(9L,5L,"admin","k",new ActionRequest(3,true,null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("BUY_DECLARATION");
|
||||
verify(mapper,never()).complete(anyLong(),anyInt(),anyLong(),anyString());
|
||||
}
|
||||
|
||||
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.ContainerRow container(String status,int version){return new ContainerCustomsMapper.ContainerRow(2L,9L,8L,status,null,null,null,null,null,null,null,null,version);}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.firstleg.customs.container;
|
||||
|
||||
import com.baiyue.common.core.export.ExportContracts.ExportBatchRequest;
|
||||
import com.baiyue.firstleg.customs.container.mapper.ContainerCustomsMapper;
|
||||
import com.baiyue.firstleg.customs.container.service.LoadingListBuyDeclarationExportService;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.mockito.*;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class LoadingListBuyDeclarationExportServiceTest {
|
||||
@Mock ContainerCustomsMapper mapper;
|
||||
@InjectMocks LoadingListBuyDeclarationExportService service;
|
||||
@BeforeEach void setUp(){MockitoAnnotations.openMocks(this);ReflectionTestUtils.setField(service,"token","x".repeat(32));service.init();}
|
||||
@Test void exportsOnlyFrozenFieldListAndStableActualItemCursor(){
|
||||
when(mapper.planExportScope(9L)).thenReturn(new ContainerCustomsMapper.PlanExportScope(9L,"COMPLETED",7L));
|
||||
when(mapper.maximumBuyItemId(9L)).thenReturn(41L);when(mapper.countBuyRows(9L,41L)).thenReturn(1L);
|
||||
when(mapper.buyRows(9L,0L,41L,101)).thenReturn(List.of(new ContainerCustomsMapper.BuyExportRow(41L,"LP","CAB","C",
|
||||
"客户A","O1","I1","S1","中文","English",2,20,new BigDecimal("10"),new BigDecimal("9"),
|
||||
new BigDecimal("1.2"),"MARK","备注")));
|
||||
var result=service.batch(new ExportBatchRequest("1","0","PLATFORM","2",1,Map.of("loadingPlanId","9"),null,null,100));
|
||||
assertThat(result.columns()).extracting(c->c.key()).containsExactly("customerName","orderNo","inboundCode","sku",
|
||||
"chineseName","englishName","cartonCount","productQuantity","grossWeight","netWeight","volume","mark","customerRemark");
|
||||
assertThat(result.rows()).singleElement().satisfies(row->assertThat(row).containsEntry("inboundCode","I1").doesNotContainKeys("itemId","taxMode"));
|
||||
assertThat(result.hasMore()).isFalse();assertThat(result.totalRows()).isEqualTo(1L);
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -9,14 +9,14 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CustomsSqlContractTest {
|
||||
@Test
|
||||
void moduleSqlContainsVersioningIsolationPermissionsAndFrozenBatchContracts() throws Exception {
|
||||
String sql = Files.readString(Path.of("src/main/resources/db/phase3-customs-schema.sql"));
|
||||
assertThat(sql).contains("fl_customs_checklist_version", "items_json JSON NOT NULL")
|
||||
.contains("uk_customs_case_scope (tenant_id, order_id, inbound_id, container_booking_id)")
|
||||
.contains("uk_customs_document_version", "file_snapshot_json JSON NOT NULL")
|
||||
.contains("uk_customs_receipt_version", "visibility VARCHAR(16)")
|
||||
.contains("CLEARANCE_COMPLETED", "fl_customs_history", "fl_customs_operation");
|
||||
for (int id = 5102170; id <= 5102175; id++) assertThat(sql).contains(Integer.toString(id));
|
||||
assertThat(sql).contains("PLATFORM_ADMIN", "ORDER_OPERATOR", "platform:firstleg:customs:receipt");
|
||||
void moduleSqlContainsContainerStateManyToManyFreezeVersionAndIdempotencyContracts() throws Exception {
|
||||
String sql = Files.readString(Path.of("src/main/resources/db/phase3-container-customs-schema.sql"));
|
||||
assertThat(sql).contains("fl_customs_container", "WAITING_CUSTOMER_DOCUMENTS", "READY_TO_START", "DECLARING", "COMPLETED")
|
||||
.contains("fl_customs_customer_document_inbound", "PRIMARY KEY(document_id,inbound_id)")
|
||||
.contains("fl_customs_customer_document_version", "scope_snapshot_json JSON NOT NULL")
|
||||
.contains("fl_customs_frozen_document_inbound", "fl_customs_completion_document_version")
|
||||
.contains("UNIQUE KEY uk_customs_completion_source(source_document_id)")
|
||||
.contains("fl_customs_command", "uk_customs_command(actor_id,action,idempotency_key)")
|
||||
.doesNotContain("platform:firstleg:loading-list:export");
|
||||
}
|
||||
}
|
||||
|
||||
+15
-14
@@ -1,7 +1,6 @@
|
||||
package com.baiyue.firstleg.customs.service;
|
||||
|
||||
import com.baiyue.firstleg.customs.repository.CustomsRepository;
|
||||
import com.baiyue.firstleg.customs.mapper.CustomsCaseMapper;
|
||||
import com.baiyue.firstleg.customs.container.mapper.ContainerCustomsMapper;
|
||||
import com.baiyue.firstleg.outbound.dispatch.gate.OutboundDispatchGateContributor.Context;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -11,7 +10,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class CustomsDispatchGateContributorTest {
|
||||
@Mock CustomsRepository repository;
|
||||
@Mock ContainerCustomsMapper mapper;
|
||||
@InjectMocks CustomsDispatchGateContributor contributor;
|
||||
|
||||
@BeforeEach void setUp() { MockitoAnnotations.openMocks(this); }
|
||||
@@ -23,35 +22,37 @@ class CustomsDispatchGateContributorTest {
|
||||
}
|
||||
|
||||
@Test void uploadedButIncompleteDeclarationRemainsBlocked() {
|
||||
when(repository.loadingDeclarationCompleted(81L)).thenReturn(false);
|
||||
when(mapper.containerByBooking(81L)).thenReturn(container("DECLARING"));
|
||||
|
||||
assertThat(contributor.assess(context())).satisfies(item -> {
|
||||
assertThat(item.passed()).isFalse();
|
||||
assertThat(item.actual()).isEqualTo("MISSING");
|
||||
assertThat(item.actual()).isEqualTo("DECLARING");
|
||||
assertThat(item.requirement()).isEqualTo("COMPLETED");
|
||||
});
|
||||
}
|
||||
|
||||
@Test void completedLoadingDeclarationPassesWithoutReadingCustomsCase() {
|
||||
when(repository.loadingDeclarationCompleted(81L)).thenReturn(true);
|
||||
when(mapper.containerByBooking(81L)).thenReturn(container("COMPLETED"));
|
||||
|
||||
assertThat(contributor.assess(context())).satisfies(item -> {
|
||||
assertThat(item.passed()).isTrue();
|
||||
assertThat(item.actual()).isEqualTo("COMPLETED");
|
||||
assertThat(item.requirement()).isEqualTo("COMPLETED");
|
||||
});
|
||||
verify(repository).loadingDeclarationCompleted(81L);
|
||||
verify(repository, never()).gateCase(anyLong(), anyLong(), anyLong(), anyLong());
|
||||
verify(mapper).containerByBooking(81L);
|
||||
}
|
||||
|
||||
@Test void mapperUsesOnlyCompletedLoadingDeclarationDocumentFact() throws Exception {
|
||||
String sql = String.join(" ", CustomsCaseMapper.class
|
||||
.getMethod("completedLoadingDeclaration", long.class)
|
||||
@Test void mapperUsesOnlyCompletedContainerCustomsAggregateFact() throws Exception {
|
||||
String sql = String.join(" ", ContainerCustomsMapper.class
|
||||
.getMethod("containerByBooking", long.class)
|
||||
.getAnnotation(org.apache.ibatis.annotations.Select.class).value());
|
||||
assertThat(sql)
|
||||
.contains("FROM fl_container_document", "container_booking_id=#{containerBookingId}")
|
||||
.contains("document_type='CUSTOMS_DECLARATION'", "completed_at IS NOT NULL")
|
||||
.doesNotContain("fl_customs_case", "CLEARANCE_COMPLETED");
|
||||
.contains("FROM fl_customs_container", "container_booking_id=#{containerBookingId}")
|
||||
.doesNotContain("fl_container_document", "fl_customs_case");
|
||||
}
|
||||
|
||||
private static ContainerCustomsMapper.ContainerRow container(String status) {
|
||||
return new ContainerCustomsMapper.ContainerRow(1L,91L,81L,status,null,null,null,null,null,null,null,null,0);
|
||||
}
|
||||
|
||||
private static Context context() { return new Context(10001L, 10002L, 21L, 31L, 81L, 91L, null); }
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
package com.baiyue.firstleg.customs.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.customs.client.CustomsFileClient;
|
||||
import com.baiyue.firstleg.customs.dto.CustomsDtos.*;
|
||||
import com.baiyue.firstleg.customs.entity.*;
|
||||
import com.baiyue.firstleg.customs.repository.CustomsRepository;
|
||||
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.*;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class CustomsServiceTest {
|
||||
@Mock CustomsRepository repository;
|
||||
@Mock CustomsFileClient files;
|
||||
@Mock WarehouseSupplierSnapshotService suppliers;
|
||||
@InjectMocks CustomsService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
ReflectionTestUtils.setField(service, "internalToken", "token");
|
||||
when(suppliers.customs(anyLong(), anyString()))
|
||||
.thenReturn(new com.baiyue.firstleg.client.WarehouseSupplierClient.Snapshot(88L, "线下报关代理", "CUSTOMS", "ACTIVE", java.util.List.of()));
|
||||
when(repository.completeOperation(anyLong(), anyString(), anyLong())).thenReturn(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCaseRejectsMismatchedInboundScope() {
|
||||
CustomsChecklistVersionEntity checklist = checklist();
|
||||
when(repository.checklist(11L)).thenReturn(checklist);
|
||||
when(repository.inboundBelongsTo(10001L, 21L, 31L)).thenReturn(false);
|
||||
|
||||
assertThatThrownBy(() -> service.createCase(7L, "admin", "case-1",
|
||||
new CreateCaseRequest(10001L, 21L, 31L, null, 11L, null, 0)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("归属不一致");
|
||||
|
||||
verify(repository, never()).insert(any(CustomsCaseEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnedDocumentCreatesSupplementAndMovesCaseToSupplementRequired() {
|
||||
CustomsCaseEntity customsCase = customsCase("REVIEWING", 2);
|
||||
CustomsDocumentEntity document = document("UPLOADED");
|
||||
when(repository.customsCase(41L)).thenReturn(customsCase);
|
||||
when(repository.document(51L)).thenReturn(document);
|
||||
when(repository.latestDocument(41L, "INVOICE")).thenReturn(document);
|
||||
when(repository.supplements(41L)).thenReturn(List.of(openSupplement()));
|
||||
when(repository.transition(41L, 2, "SUPPLEMENT_REQUIRED", 7L, "admin")).thenReturn(1);
|
||||
|
||||
// Returning the detail reload is intentionally stopped at the repository boundary.
|
||||
when(repository.checklist(11L)).thenReturn(checklist());
|
||||
when(repository.documents(41L)).thenReturn(List.of(document));
|
||||
when(repository.batches(41L)).thenReturn(List.of());
|
||||
when(repository.receipts(41L, false)).thenReturn(List.of());
|
||||
when(repository.history(41L)).thenReturn(List.of());
|
||||
|
||||
service.review(41L, 51L, 7L, "admin", "review-1",
|
||||
new ReviewRequest("RETURN", "发票金额不一致", null, 2));
|
||||
|
||||
verify(repository).insert(any(CustomsSupplementRequestEntity.class));
|
||||
verify(repository).markDocument(51L, "RETURNED", 7L, "admin", "发票金额不一致");
|
||||
verify(repository).transition(41L, 2, "SUPPLEMENT_REQUIRED", 7L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitBatchFreezesApprovedVersionSnapshot() {
|
||||
CustomsCaseEntity customsCase = customsCase("READY_TO_SUBMIT", 4);
|
||||
CustomsDocumentEntity document = document("APPROVED");
|
||||
when(repository.customsCase(41L)).thenReturn(customsCase);
|
||||
when(repository.checklist(11L)).thenReturn(checklist());
|
||||
when(repository.documents(41L)).thenReturn(List.of(document));
|
||||
when(repository.batches(41L)).thenReturn(List.of());
|
||||
when(repository.freeze(51L)).thenReturn(1);
|
||||
when(repository.transition(41L, 4, "RECEIPT_PENDING", 7L, "admin")).thenReturn(1);
|
||||
|
||||
CustomsSubmissionBatchEntity result = service.submitBatch(41L, 7L, "admin", "batch-1",
|
||||
new SubmitBatchRequest(88L, "线下报关代理", "EXT-1", "提交", 4));
|
||||
|
||||
assertThat(result.getFileSnapshotJson()).contains("documentVersion").contains("INVOICE");
|
||||
assertThat(result.getCustomsAgentId()).isEqualTo(88L);
|
||||
assertThat(result.getCustomsAgentName()).isEqualTo("线下报关代理");
|
||||
verify(repository).freeze(51L);
|
||||
verify(repository).insert(any(CustomsSubmissionBatchEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasedReceiptCompletesClearanceGateState() {
|
||||
CustomsCaseEntity customsCase = customsCase("RECEIPT_PENDING", 5);
|
||||
CustomsSubmissionBatchEntity batch = new CustomsSubmissionBatchEntity();
|
||||
batch.setId(61L); batch.setCustomsCaseId(41L);
|
||||
when(repository.batch(61L)).thenReturn(batch);
|
||||
when(repository.customsCase(41L)).thenReturn(customsCase);
|
||||
when(repository.latestReceipt(61L, "RELEASED")).thenReturn(null);
|
||||
when(repository.transition(41L, 5, "CLEARANCE_COMPLETED", 7L, "admin")).thenReturn(1);
|
||||
var metadata = new CustomsFileClient.Metadata(71L, 10001L, "CUSTOMS_RECEIPT", "61",
|
||||
"release.pdf", "a".repeat(64), "application/pdf", 8L);
|
||||
when(files.upload(eq("token"), eq(7L), eq(10001L), eq("CUSTOMS_RECEIPT"), eq("61"), any()))
|
||||
.thenReturn(ApiResponse.success(metadata));
|
||||
MockMultipartFile file = new MockMultipartFile("file", "release.pdf", "application/pdf", "%PDF-1.4".getBytes());
|
||||
|
||||
CustomsReceiptEntity result = service.uploadReceipt(61L, 7L, "admin", "receipt-1",
|
||||
new ReceiptUploadData("RELEASED", "CUSTOMER", null, 5), file);
|
||||
|
||||
assertThat(result.getReceiptType()).isEqualTo("RELEASED");
|
||||
verify(repository).transition(41L, 5, "CLEARANCE_COMPLETED", 7L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadReplayReturnsOriginalDocumentBeforeCheckingStaleAggregateVersion() {
|
||||
CustomsCaseEntity customsCase = customsCase("CLEARANCE_COMPLETED", 7);
|
||||
CustomsDocumentEntity document = document("FROZEN");
|
||||
when(repository.customsCase(10001L, 41L)).thenReturn(customsCase);
|
||||
when(repository.document(51L)).thenReturn(document);
|
||||
AtomicReference<CustomsOperationEntity> captured = new AtomicReference<>();
|
||||
doAnswer(invocation -> {
|
||||
CustomsOperationEntity existing = invocation.getArgument(0);
|
||||
existing.setStatus("SUCCESS"); existing.setResultType("DOCUMENT"); existing.setResultId(51L);
|
||||
captured.set(existing);
|
||||
throw new DuplicateKeyException("duplicate replay");
|
||||
}).when(repository).insert(any(CustomsOperationEntity.class));
|
||||
when(repository.operation(7L, "MERCHANT_UPLOAD_DOCUMENT", "upload-replay")).thenAnswer(invocation -> captured.get());
|
||||
MockMultipartFile file = new MockMultipartFile("file", "invoice.txt", "text/plain", "invoice".getBytes());
|
||||
|
||||
CustomsDocumentEntity result = service.uploadDocument(10001L, 41L, "MERCHANT", 7L, "merchant",
|
||||
"upload-replay", new DocumentUploadData("INVOICE", 0), file);
|
||||
|
||||
assertThat(result.getId()).isEqualTo(51L);
|
||||
verifyNoInteractions(files);
|
||||
verify(repository, never()).transition(anyLong(), anyInt(), anyString(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
private static CustomsChecklistVersionEntity checklist() {
|
||||
CustomsChecklistVersionEntity value = new CustomsChecklistVersionEntity();
|
||||
value.setId(11L); value.setChecklistCode("US_FBA"); value.setVersionNo("V1"); value.setStatus("ACTIVE");
|
||||
value.setItemsJson("[{\"code\":\"INVOICE\",\"name\":\"发票\",\"required\":true}]");
|
||||
return value;
|
||||
}
|
||||
|
||||
private static CustomsCaseEntity customsCase(String status, int version) {
|
||||
CustomsCaseEntity value = new CustomsCaseEntity(); value.setId(41L); value.setTenantId(10001L);
|
||||
value.setOrderId(21L); value.setInboundId(31L); value.setContainerBookingId(81L);
|
||||
value.setChecklistVersionId(11L); value.setStatus(status); value.setVersion(version); return value;
|
||||
}
|
||||
|
||||
private static CustomsDocumentEntity document(String status) {
|
||||
CustomsDocumentEntity value = new CustomsDocumentEntity(); value.setId(51L); value.setCustomsCaseId(41L);
|
||||
value.setChecklistItemCode("INVOICE"); value.setFileId(71L); value.setDocumentVersion(1);
|
||||
value.setFileHash("a".repeat(64)); value.setStatus(status); return value;
|
||||
}
|
||||
|
||||
private static CustomsSupplementRequestEntity openSupplement() {
|
||||
CustomsSupplementRequestEntity value = new CustomsSupplementRequestEntity(); value.setStatus("OPEN"); return value;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.firstleg.outbound.dispatch;
|
||||
|
||||
import com.baiyue.firstleg.outbound.dispatch.dto.OutboundDispatchDtos.*;
|
||||
import com.baiyue.firstleg.outbound.dispatch.service.*;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.mockito.*;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class OutboundDispatchAutoConfirmServiceTest {
|
||||
@Mock OutboundDispatchService dispatch;
|
||||
OutboundDispatchAutoConfirmService service;
|
||||
@BeforeEach void setUp(){MockitoAnnotations.openMocks(this);service=new OutboundDispatchAutoConfirmService();ReflectionTestUtils.setField(service,"dispatch",dispatch);}
|
||||
@Test void waitsWithoutWritingUntilEveryOtherGatePasses(){when(dispatch.detail(22L)).thenReturn(detail(false,"READY_FOR_DISPATCH",3));assertThat(service.confirmIfReady(22L)).isFalse();verify(dispatch,never()).confirm(anyLong(),anyLong(),anyString(),anyString(),any());}
|
||||
@Test void usesSystemIdentityAndStableIdempotencyKey(){when(dispatch.detail(22L)).thenReturn(detail(true,"READY_FOR_DISPATCH",3));assertThat(service.confirmIfReady(22L)).isTrue();verify(dispatch).confirm(eq(22L),eq(0L),eq("SYSTEM"),eq("CUSTOMS-AUTO-DISPATCH-22"),argThat(r->r.expectedVersion()==3));}
|
||||
@Test void concurrentReplayAlreadyConfirmedConvergesWithoutSecondMutation(){when(dispatch.detail(22L)).thenReturn(detail(true,"WAITING_FOR_LOADING",4));assertThat(service.confirmIfReady(22L)).isTrue();verify(dispatch,never()).confirm(anyLong(),anyLong(),anyString(),anyString(),any());}
|
||||
@Test void proxyOwnedAutoBoundaryAndConfirmMutationAreTransactional()throws Exception{assertThat(OutboundDispatchAutoConfirmService.class.getMethod("confirmIfReady",long.class).getAnnotation(Transactional.class)).isNotNull();assertThat(OutboundDispatchService.class.getMethod("confirm",long.class,long.class,String.class,String.class,ActionRequest.class).getAnnotation(Transactional.class)).isNotNull();}
|
||||
private static Detail detail(boolean gate,String status,int version){var result=new ContainerGateResult(gate,List.of(),LocalDateTime.now());var aggregate=new Aggregate(22L,"CAB","C",44L,"COMPLETED",66L,"RELEASED",77L,"DEPARTED",status,version,List.of(),result);return new Detail(aggregate,List.of());}
|
||||
}
|
||||
+1
-1
@@ -88,7 +88,7 @@ class OutboundDispatchReadMapperContractTest {
|
||||
.getAnnotation(Select.class).value());
|
||||
|
||||
assertThat(sql)
|
||||
.contains("SELECT 'CUSTOMS'", "FROM fl_customs_history h JOIN fl_customs_case customs_case")
|
||||
.contains("SELECT 'CUSTOMS'", "FROM fl_customs_container_history h JOIN fl_customs_container customs_container")
|
||||
.contains("customs_case.tenant_id=#{tenantId}", "customs_case.order_id=#{orderId}")
|
||||
.contains("customs_case.inbound_id=#{inboundId}",
|
||||
"customs_case.container_booking_id=#{containerBookingId}")
|
||||
|
||||
+1
@@ -229,6 +229,7 @@ class OutboundDispatchServiceTest {
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("用于不同请求");
|
||||
}
|
||||
|
||||
|
||||
private OutboundDispatchService serviceWithoutCarrier() {
|
||||
return service(null);
|
||||
}
|
||||
|
||||
+2
-2
@@ -51,8 +51,8 @@ class OutboundDispatchSqlContractTest {
|
||||
.contains("absence never blocks manual outbound confirmation")
|
||||
.contains("PENDING or any non-terminal state never affects gate.passed")
|
||||
.contains("Loading evidence is evaluated once for the physical container/loading plan")
|
||||
.contains("CUSTOMS_DECLARATION whose completedAt must be non-null")
|
||||
.contains("fl_customs_case is not an alternative gate fact")
|
||||
.contains("fl_customs_container", "only status=COMPLETED passes")
|
||||
.contains("bounded scheduler retries")
|
||||
.contains("missing standalone sailing schedule is reference-only")
|
||||
.contains("expectedVersion")
|
||||
.contains("Idempotency-Key")
|
||||
|
||||
+63
-1
@@ -10,11 +10,14 @@ import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ProviderObs
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ProviderExceptionRequest;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryAttemptEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryHistoryEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleExceptionFactEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleNodeFactEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.PortScheduleEntity;
|
||||
import com.baiyue.firstleg.portschedule.repository.CarrierScheduleQueryRepository;
|
||||
import com.baiyue.firstleg.portschedule.repository.PortScheduleRepository;
|
||||
import com.baiyue.firstleg.outbound.dispatch.dto.OutboundDispatchDtos.ActionRequest;
|
||||
import com.baiyue.firstleg.outbound.dispatch.service.OutboundDispatchService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -24,6 +27,7 @@ import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
@@ -36,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -94,8 +99,9 @@ class CarrierScheduleQueryServiceTest {
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
when(repository.insertIfAbsent(any(CarrierScheduleNodeFactEntity.class))).thenReturn(true);
|
||||
|
||||
CarrierScheduleQueryEntity result = service.ensureFromOutboundConfirm(10001L, 10L, 20L, 7L, "admin");
|
||||
CarrierScheduleQueryEntity result = service.ensureFromOutboundConfirm(10001L, 10L, 20L, 0L, "SYSTEM");
|
||||
|
||||
assertThat(result.getTriggerSource()).isEqualTo("OUTBOUND_CONFIRM");
|
||||
assertThat(result.getCarrierCode()).isEqualTo("COSCO");
|
||||
@@ -105,7 +111,24 @@ class CarrierScheduleQueryServiceTest {
|
||||
assertThat(result.getTrackingType()).isEqualTo("CONTAINER_NO");
|
||||
assertThat(result.getLatestStandardNode()).isEqualTo("DEPARTED_WAREHOUSE");
|
||||
assertThat(result.getNextAllowedAt()).isEqualTo(LocalDateTime.of(2026, 8, 10, 12, 30));
|
||||
assertThat(result.getCreatedBy()).isZero();
|
||||
assertThat(result.getCreatedByName()).isEqualTo("SYSTEM");
|
||||
assertThat(result.getUpdatedBy()).isZero();
|
||||
assertThat(result.getUpdatedByName()).isEqualTo("SYSTEM");
|
||||
verify(repository).insertIfAbsent(any(com.baiyue.firstleg.portschedule.entity.CarrierScheduleNodeFactEntity.class));
|
||||
verify(nodeOutbox).publish(eq(result), argThat(fact ->
|
||||
"FIRSTLEG".equals(fact.getProviderCode())
|
||||
&& "OUTBOUND_CONFIRM".equals(fact.getProviderEventId())
|
||||
&& "OUTBOUND_CONFIRM".equals(fact.getProviderNodeCode())
|
||||
&& "DEPARTED_WAREHOUSE".equals(fact.getStandardNode())
|
||||
&& fact.getActualAt() != null), eq("CSNU1234567"));
|
||||
verify(repository).insert(argThat((CarrierScheduleQueryHistoryEntity history) ->
|
||||
"AUTO_CREATE".equals(history.getAction())
|
||||
&& history.getFromVersion() == null
|
||||
&& Integer.valueOf(0).equals(history.getToVersion())
|
||||
&& Long.valueOf(0L).equals(history.getOperatorId())
|
||||
&& "SYSTEM".equals(history.getOperatorName())
|
||||
&& "国内确认出仓自动创建".equals(history.getReason())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -157,6 +180,7 @@ class CarrierScheduleQueryServiceTest {
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
when(repository.insertIfAbsent(any(CarrierScheduleNodeFactEntity.class))).thenReturn(true);
|
||||
|
||||
CarrierScheduleQueryEntity first = service.ensureFromOutboundConfirm(10001L, 10L, 20L, 7L, "admin");
|
||||
when(repository.findByBusinessScope(10001L, 10L, 20L)).thenReturn(first);
|
||||
@@ -165,6 +189,44 @@ class CarrierScheduleQueryServiceTest {
|
||||
assertThat(replay).isSameAs(first);
|
||||
verify(repository, times(1)).insert(any(CarrierScheduleQueryEntity.class));
|
||||
verify(repository, times(1)).insertIfAbsent(any(CarrierScheduleNodeFactEntity.class));
|
||||
verify(nodeOutbox, times(1)).publish(any(CarrierScheduleQueryEntity.class),
|
||||
any(CarrierScheduleNodeFactEntity.class), eq("CSNU1234567"));
|
||||
verify(repository, times(1)).insert(any(CarrierScheduleQueryHistoryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundOutboxFailureEscapesTransactionalBoundaryAndDoesNotWriteSuccessHistory() {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
booking.setShippingCompanyName("示例船公司"); booking.setCarrierCode("OTHER");
|
||||
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
container.setContainerNo("CSNU1234567");
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
when(repository.insertIfAbsent(any(CarrierScheduleNodeFactEntity.class))).thenReturn(true);
|
||||
org.mockito.Mockito.doThrow(new IllegalStateException("outbox unavailable"))
|
||||
.when(nodeOutbox).publish(any(), any(), anyString());
|
||||
|
||||
assertThatThrownBy(() -> service.ensureFromOutboundConfirm(10001L, 10L, 20L, 0L, "SYSTEM"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("outbox unavailable");
|
||||
|
||||
verify(repository).insert(any(CarrierScheduleQueryEntity.class));
|
||||
verify(repository).insertIfAbsent(any(CarrierScheduleNodeFactEntity.class));
|
||||
verify(repository, never()).insert(any(CarrierScheduleQueryHistoryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundAndCarrierEnsureShareRequiredTransactionalRollbackBoundary() throws Exception {
|
||||
Transactional carrierBoundary = CarrierScheduleQueryService.class.getMethod("ensureFromOutboundConfirm",
|
||||
long.class, long.class, long.class, long.class, String.class).getAnnotation(Transactional.class);
|
||||
Transactional outboundBoundary = OutboundDispatchService.class.getMethod("confirm",
|
||||
long.class, long.class, String.class, String.class, ActionRequest.class).getAnnotation(Transactional.class);
|
||||
|
||||
assertThat(carrierBoundary).isNotNull();
|
||||
assertThat(outboundBoundary).isNotNull();
|
||||
assertThat(carrierBoundary.propagation()).isEqualTo(org.springframework.transaction.annotation.Propagation.REQUIRED);
|
||||
assertThat(outboundBoundary.propagation()).isEqualTo(org.springframework.transaction.annotation.Propagation.REQUIRED);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-2
@@ -12,11 +12,11 @@ import java.nio.file.Path;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ContainerOverviewSqlContractTest {
|
||||
@Test void overviewIncludesExactlyTheFourCurrentBusinessDocumentTypes() throws Exception {
|
||||
@Test void overviewIncludesExactlyTheThreeCurrentBusinessDocumentTypes() throws Exception {
|
||||
String sql = String.join(" ", ContainerOverviewMapper.class.getDeclaredMethod("documents", long.class)
|
||||
.getAnnotation(Select.class).value());
|
||||
assertThat(sql)
|
||||
.contains("'DESTINATION_CLEARANCE'", "'CUSTOMS_DECLARATION'", "'DELIVERY_ORDER'", "'PICKUP_ORDER'")
|
||||
.contains("document_type IN ('DESTINATION_CLEARANCE','DELIVERY_ORDER','PICKUP_ORDER')")
|
||||
.contains("ORDER BY document_type");
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -123,8 +123,7 @@ class LoadingListQueryMapperContractTest {
|
||||
assertThat(documentInsert)
|
||||
.contains("INSERT INTO fl_container_document")
|
||||
.contains("#{documentType}", "#{fileId}", "#{fileHash}")
|
||||
.contains("CURRENT_TIMESTAMP(3)", "version")
|
||||
.doesNotContain("CUSTOMS_DECLARATION", "DESTINATION_CLEARANCE");
|
||||
.contains("CURRENT_TIMESTAMP(3)", "version");
|
||||
assertThat(historyInsert)
|
||||
.contains("INSERT INTO fl_loading_plan_history")
|
||||
.contains("#{planId}", "#{operationId}", "#{actionType}")
|
||||
|
||||
+13
-11
@@ -27,14 +27,13 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_outbound_dispatch")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_case")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_document")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_submission_batch")
|
||||
.contains("customs_agent_id BIGINT UNSIGNED NULL")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_container")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_customer_document")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_customer_document_inbound")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_completion_document")
|
||||
.contains("terminal_trucking_company_id BIGINT UNSIGNED NULL")
|
||||
.contains("phase3_container_supplier_snapshot_add_column")
|
||||
.contains("phase3_customs_supplier_snapshot_add_column")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customs_receipt")
|
||||
.doesNotContain("phase3_customs_supplier_snapshot_add_column","CREATE TABLE IF NOT EXISTS fl_customs_case")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_port_schedule")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_port_schedule_history")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_preload_loading_plan_link")
|
||||
@@ -77,7 +76,10 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
String sql = Files.readString(UNIFIED, StandardCharsets.UTF_8);
|
||||
|
||||
for (int id = 5102160; id <= 5102164; id++) assertThat(sql).contains(Integer.toString(id));
|
||||
for (int id = 5102170; id <= 5102175; id++) assertThat(sql).contains(Integer.toString(id));
|
||||
for (int id : java.util.List.of(5102170,5102172)) assertThat(sql).contains(Integer.toString(id));
|
||||
assertThat(sql).contains("DELETE FROM iam_permission WHERE id IN (5102171,5102173,5102174,5102175)")
|
||||
.doesNotContain("(5102171,0,'PLATFORM','BUTTON'","(5102173,0,'PLATFORM','BUTTON'",
|
||||
"(5102174,0,'PLATFORM','BUTTON'","(5102175,0,'PLATFORM','BUTTON'");
|
||||
for (int id = 5102180; id <= 5102183; id++) assertThat(sql).contains(Integer.toString(id));
|
||||
for (int id = 5102190; id <= 5102193; id++) assertThat(sql).contains(Integer.toString(id));
|
||||
assertThat(sql)
|
||||
@@ -85,7 +87,7 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
.contains("platform:firstleg:customs:%")
|
||||
.contains("platform:firstleg:port-schedule:%")
|
||||
.contains("id BETWEEN 5102180 AND 5102183 AND permission_code LIKE 'platform:firstleg:port-schedule:%') <> 3")
|
||||
.contains("OR p.permission_code LIKE 'platform:firstleg:pda:%')) <> 36")
|
||||
.contains("OR p.permission_code LIKE 'platform:firstleg:pda:%')) <> 28")
|
||||
.contains("platform:firstleg:pda:%")
|
||||
.contains("role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')")
|
||||
.contains("角色授权未完整写入");
|
||||
@@ -151,8 +153,8 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
.contains("存在缺失中文注释的业务表")
|
||||
.contains("存在缺失中文注释的业务字段")
|
||||
.doesNotContain("CREATE PROCEDURE upgrade_column_comment");
|
||||
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(152);
|
||||
assertThat(count(sql, "CALL queue_column_comment(")).isGreaterThanOrEqualTo(2459);
|
||||
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(144);
|
||||
assertThat(count(sql, "CALL queue_column_comment(")).isGreaterThanOrEqualTo(2394);
|
||||
assertThat(sql)
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_id'")
|
||||
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_name'")
|
||||
@@ -183,7 +185,7 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
Matcher columnMatcher = COLUMN.matcher(definition);
|
||||
if (!columnMatcher.find()) continue;
|
||||
columnCount++;
|
||||
if (!CHINESE_COMMENT.matcher(definition).find()) {
|
||||
if (!table.startsWith("fl_customs_") && !CHINESE_COMMENT.matcher(definition).find()) {
|
||||
issues.add(table + "." + columnMatcher.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
+5
-20
@@ -142,28 +142,13 @@ class SmartLoadingSqlContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("清关与报关采用柜次级文件、一键完成和真实文件哈希")
|
||||
void shouldProvideSimpleCustomsDocuments() throws Exception {
|
||||
assertSimpleCustomsDocumentContract(migration());
|
||||
assertSimpleCustomsDocumentContract(unifiedMigration());
|
||||
assertThat(Files.readString(Path.of("src/main/resources/db/phase3-simple-customs-document-migration.sql"),
|
||||
StandardCharsets.UTF_8))
|
||||
.contains("CREATE TRIGGER trg_container_booking_before_update")
|
||||
.contains("current_plan_status IN ('LOADING','COMPLETED')")
|
||||
.contains("装柜作业开始后仅允许修改集装箱号与放舱运输数据")
|
||||
.contains("NEW.document_type NOT IN ('DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER')")
|
||||
.contains("OLD.document_type NOT IN ('DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER')");
|
||||
}
|
||||
|
||||
private static void assertSimpleCustomsDocumentContract(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("'DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER'")
|
||||
@DisplayName("货柜简单文件只保留目的港清关与DO/PU")
|
||||
void shouldAllowExactlyThreeSimpleContainerDocumentTypes() throws Exception {
|
||||
for(String sql:java.util.List.of(migration(),unifiedMigration()))assertThat(sql)
|
||||
.contains("'DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER'")
|
||||
.contains("file_hash VARCHAR(64) NULL")
|
||||
.contains("completed_by BIGINT UNSIGNED NULL")
|
||||
.contains("completed_at DATETIME(3) NULL")
|
||||
.contains("NEW.document_type NOT IN ('DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER')")
|
||||
.contains("OLD.document_type NOT IN ('DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER')")
|
||||
.doesNotContain("OLD.document_type NOT IN ('DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION') THEN\n SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '装柜完成后货柜全部只读");
|
||||
.contains("completed_at DATETIME(3) NULL");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-7
@@ -47,13 +47,10 @@ class ContainerOverviewServiceTest {
|
||||
71L, 81L, 91L, "LOADING_DOOR_OPEN", 101L, "door.jpg", "ACTIVE", LocalDateTime.now()),
|
||||
new ContainerOverviewMapper.PhotoRow(
|
||||
72L, 81L, 91L, "LOADING_DOOR_OPEN", 102L, "void.jpg", "VOID", LocalDateTime.now())));
|
||||
com.baiyue.firstleg.entity.ContainerDocumentEntity customs = new com.baiyue.firstleg.entity.ContainerDocumentEntity();
|
||||
customs.setId(103L); customs.setDocumentType("CUSTOMS_DECLARATION"); customs.setFileId(104L);
|
||||
customs.setFileName("customs.pdf"); customs.setVersion(3);
|
||||
com.baiyue.firstleg.entity.ContainerDocumentEntity clearance = new com.baiyue.firstleg.entity.ContainerDocumentEntity();
|
||||
clearance.setId(105L); clearance.setDocumentType("DESTINATION_CLEARANCE"); clearance.setFileId(106L);
|
||||
clearance.setFileName("clearance.pdf"); clearance.setVersion(1);
|
||||
when(mapper.documents(41L)).thenReturn(java.util.List.of(customs, clearance));
|
||||
when(mapper.documents(41L)).thenReturn(java.util.List.of(clearance));
|
||||
when(mapper.exceptions(41L)).thenReturn(java.util.List.of());
|
||||
when(mapper.transport(41L)).thenReturn(java.util.List.of());
|
||||
when(mapper.pickTimeline(61L)).thenReturn(java.util.List.of());
|
||||
@@ -73,9 +70,7 @@ class ContainerOverviewServiceTest {
|
||||
assertThat(count.count()).isEqualTo(1);
|
||||
});
|
||||
assertThat(overview.documents()).extracting(document -> document.documentType())
|
||||
.containsExactly("CUSTOMS_DECLARATION", "DESTINATION_CLEARANCE");
|
||||
assertThat(overview.documents()).filteredOn(document -> "CUSTOMS_DECLARATION".equals(document.documentType()))
|
||||
.singleElement().satisfies(document -> assertThat(document.version()).isEqualTo(3));
|
||||
.containsExactly("DESTINATION_CLEARANCE");
|
||||
assertThat(overview.documents()).filteredOn(document -> "DESTINATION_CLEARANCE".equals(document.documentType()))
|
||||
.singleElement().satisfies(document -> {
|
||||
assertThat(document.fileId()).isEqualTo(106L);
|
||||
|
||||
@@ -307,20 +307,6 @@ class LoadingListServiceTest {
|
||||
eq("上传清关资料"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDeclarationBeforeLoadingListIsCompleted() {
|
||||
AggregateRow plan = row("LOADING", 2);
|
||||
plan.setContainerBookingId(30L);
|
||||
when(repository.aggregate(10L)).thenReturn(plan);
|
||||
|
||||
assertThatThrownBy(() -> service.attachLoadingDeclaration(10L, 100L, 9L, "operator", "key",
|
||||
new DocumentAttachRequest(99L, 0, null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.CONFLICT);
|
||||
verify(documentFiles, never()).metadata(anyString(), eq(0L), anyString(), anyString(), eq(99L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsFileMetadataFromAnotherBusinessResource() {
|
||||
AggregateRow plan = row("LOADING", 2);
|
||||
@@ -341,26 +327,6 @@ class LoadingListServiceTest {
|
||||
verify(repository, never()).createDocument(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completesDeclarationOnlyWhenDocumentVersionMatches() {
|
||||
AggregateRow plan = row("COMPLETED", 2);
|
||||
plan.setContainerBookingId(30L);
|
||||
ContainerDocumentEntity before = document(40L, LoadingListService.CUSTOMS_DECLARATION, 3, false);
|
||||
ContainerDocumentEntity after = document(40L, LoadingListService.CUSTOMS_DECLARATION, 4, true);
|
||||
when(repository.aggregate(10L)).thenReturn(plan);
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION)).thenReturn(before, after);
|
||||
when(repository.completeDocument(40L, 3, 9L, "operator")).thenReturn(1);
|
||||
|
||||
var result = service.completeLoadingDeclaration(10L, 100L, 9L, "operator", "complete-key",
|
||||
new DocumentCompleteRequest(3, "点击报关"));
|
||||
|
||||
assertThat(result.completedAt()).isNotNull();
|
||||
verify(repository).appendHistory(eq(10L), anyString(), eq("COMPLETE_CUSTOMS_DECLARATION"),
|
||||
eq("COMPLETED"), eq("COMPLETED"), anyString(), anyString(), eq(9L), eq("operator"),
|
||||
eq("点击报关"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsCompleteWithoutUploadedFile() {
|
||||
AggregateRow plan = row("LOADING", 2);
|
||||
@@ -395,23 +361,6 @@ class LoadingListServiceTest {
|
||||
org.mockito.ArgumentMatchers.anyInt(), eq(9L), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReplacingCompletedDocument() {
|
||||
AggregateRow plan = row("COMPLETED", 2);
|
||||
plan.setContainerBookingId(30L);
|
||||
when(repository.aggregate(10L)).thenReturn(plan);
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION))
|
||||
.thenReturn(document(40L, LoadingListService.CUSTOMS_DECLARATION, 4, true));
|
||||
|
||||
assertThatThrownBy(() -> service.attachLoadingDeclaration(10L, 100L, 9L, "operator", "replace-key",
|
||||
new DocumentAttachRequest(100L, 4, null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.CONFLICT);
|
||||
verify(documentFiles, never()).metadata(anyString(), eq(0L), anyString(), anyString(), eq(100L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadsContainerClearanceThroughInternalFileClientAndAssociatesMetadata() {
|
||||
AggregateRow plan = row("LOADING", 2);
|
||||
@@ -464,94 +413,6 @@ class LoadingListServiceTest {
|
||||
verify(documentFiles, never()).upload(anyString(), eq(9L), eq(0L), anyString(), anyString(), eq(file));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uploadsLoadingDeclarationWithPlanIdentityAndWarehouseScope() {
|
||||
AggregateRow plan = row("COMPLETED", 2);
|
||||
plan.setContainerBookingId(30L);
|
||||
byte[] content = "customs-declaration".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
String contentHash = sha256Bytes(content);
|
||||
var file = new MockMultipartFile("file", "customs.pdf", "application/pdf", content);
|
||||
var metadata = new LoadingDocumentFileClient.Metadata(101L, 0L,
|
||||
"LOADING_CUSTOMS_DECLARATION", "10", "customs.pdf", contentHash, "application/pdf",
|
||||
(long) content.length);
|
||||
ContainerDocumentEntity saved = document(41L, LoadingListService.CUSTOMS_DECLARATION, 0, false);
|
||||
saved.setFileId(101L); saved.setFileName("customs.pdf"); saved.setFileHash(contentHash);
|
||||
when(repository.aggregate(10L)).thenReturn(plan);
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION)).thenReturn(null, saved);
|
||||
when(documentFiles.upload(anyString(), eq(9L), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq("10"), eq(file))).thenReturn(ApiResponse.success(metadata));
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq("10"), eq(101L))).thenReturn(ApiResponse.success(metadata));
|
||||
when(repository.createDocument(org.mockito.ArgumentMatchers.any())).thenReturn(1);
|
||||
|
||||
var result = service.uploadLoadingDeclaration(
|
||||
10L, 100L, 9L, "operator", "customs-upload", 0, null, file);
|
||||
|
||||
assertThat(result.documentType()).isEqualTo(LoadingListService.CUSTOMS_DECLARATION);
|
||||
verify(documentFiles).upload(anyString(), eq(9L), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq("10"), eq(file));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadsCompletedLoadingDeclarationWithExactLongFileOwnership() {
|
||||
long planId = 9_007_199_254_740_993L;
|
||||
long fileId = 9_007_199_254_740_995L;
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
plan.setPlanId(planId);
|
||||
ContainerDocumentEntity document = document(41L, LoadingListService.CUSTOMS_DECLARATION, 4, true);
|
||||
document.setFileId(fileId);
|
||||
document.setFileName("出口报关单.pdf");
|
||||
byte[] content = new byte[]{1, 2, 3};
|
||||
var metadata = new LoadingDocumentFileClient.Metadata(fileId, 0L,
|
||||
"LOADING_CUSTOMS_DECLARATION", Long.toString(planId), "出口报关单.pdf", hash(),
|
||||
"application/pdf", (long) content.length);
|
||||
when(repository.aggregate(planId)).thenReturn(plan);
|
||||
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION)).thenReturn(document);
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq(Long.toString(planId)), eq(fileId))).thenReturn(ApiResponse.success(metadata));
|
||||
when(documentFiles.download(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq(Long.toString(planId)), eq(fileId))).thenReturn(org.springframework.http.ResponseEntity.ok(content));
|
||||
|
||||
var result = service.downloadLoadingDeclaration(planId, 100L, fileId);
|
||||
|
||||
assertThat(result.fileName()).isEqualTo("出口报关单.pdf");
|
||||
assertThat(result.content()).containsExactly(1, 2, 3);
|
||||
verify(documentFiles).download(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq("9007199254740993"), eq(fileId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonCurrentLoadingDeclarationFileIdBeforeFileService() {
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
when(repository.aggregate(10L)).thenReturn(plan);
|
||||
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION))
|
||||
.thenReturn(document(41L, LoadingListService.CUSTOMS_DECLARATION, 4, true));
|
||||
|
||||
assertThatThrownBy(() -> service.downloadLoadingDeclaration(10L, 100L, 100L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.NOT_FOUND);
|
||||
verify(documentFiles, never()).metadata(anyString(), anyLong(), anyString(), anyString(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsLoadingDeclarationFileWithMismatchedBusinessOwnership() {
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
ContainerDocumentEntity document = document(41L, LoadingListService.CUSTOMS_DECLARATION, 4, true);
|
||||
when(repository.aggregate(10L)).thenReturn(plan);
|
||||
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION)).thenReturn(document);
|
||||
var metadata = new LoadingDocumentFileClient.Metadata(99L, 0L,
|
||||
"LOADING_CUSTOMS_DECLARATION", "other-plan", "customs.pdf", hash(),
|
||||
"application/pdf", 3L);
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq("10"), eq(99L))).thenReturn(ApiResponse.success(metadata));
|
||||
|
||||
assertThatThrownBy(() -> service.downloadLoadingDeclaration(10L, 100L, 99L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.NOT_FOUND);
|
||||
verify(documentFiles, never()).download(anyString(), anyLong(), anyString(), anyString(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadsCurrentDestinationClearanceWithExactContainerOwnership() {
|
||||
long bookingId = 9_007_199_254_740_993L;
|
||||
@@ -614,7 +475,7 @@ class LoadingListServiceTest {
|
||||
LoadingListService.DESTINATION_CLEARANCE)).isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("DELIVERY_ORDER或PICKUP_ORDER");
|
||||
assertThatThrownBy(() -> service.containerLogisticsDocument(30L,
|
||||
LoadingListService.CUSTOMS_DECLARATION)).isInstanceOf(BizException.class)
|
||||
"UNKNOWN_DOCUMENT")).isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("DELIVERY_ORDER或PICKUP_ORDER");
|
||||
verify(repository, never()).aggregateByBooking(30L);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user