优化
This commit is contained in:
+4
@@ -21,6 +21,7 @@ class ContainerControllerContractTest {
|
||||
assertPermission("create", "platform:firstleg:container:create");
|
||||
assertPermission("updateDraft", "platform:firstleg:container:edit");
|
||||
assertPermission("updateTransport", "platform:firstleg:container:edit");
|
||||
assertPermission("uploadClearance", "platform:firstleg:customs:manage");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -33,6 +34,9 @@ class ContainerControllerContractTest {
|
||||
|
||||
assertThat(section)
|
||||
.contains("/api/platform/v1/containers/{id}/transport:")
|
||||
.contains("/api/platform/v1/containers/{id}/clearance/document/upload:")
|
||||
.contains("multipart/form-data")
|
||||
.contains("ContainerDocumentUploadRequest")
|
||||
.contains("platform:firstleg:container:list")
|
||||
.contains("EDITING")
|
||||
.contains("WAIT_LOADING")
|
||||
|
||||
+7
-1
@@ -13,6 +13,7 @@ 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.multipart.MultipartFile;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
@@ -37,6 +38,8 @@ class LoadingListControllerContractTest {
|
||||
authority("declaration", "platform:firstleg:loading-list:view", AuthenticatedUser.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);
|
||||
}
|
||||
@@ -101,6 +104,7 @@ class LoadingListControllerContractTest {
|
||||
"/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/upload",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/complete");
|
||||
Map<String, Object> components = stringMap(document.get("components"));
|
||||
Map<String, Object> schemas = stringMap(components.get("schemas"));
|
||||
@@ -108,7 +112,9 @@ class LoadingListControllerContractTest {
|
||||
"LongId", "PreloadPage", "ConfirmedPage", "Customer", "PreloadSummary",
|
||||
"ConfirmedSummary", "LoadingItem", "OperationHistory", "PreloadDetail",
|
||||
"ConfirmedDetail", "TransportUpdateRequest", "BusinessDocumentEnvelope",
|
||||
"DocumentAttachRequest", "DocumentCompleteRequest", "BusinessDocument");
|
||||
"DocumentAttachRequest", "DocumentUploadRequest", "DocumentCompleteRequest", "BusinessDocument");
|
||||
assertThat(properties(schemas, "DocumentUploadRequest"))
|
||||
.containsOnlyKeys("file", "expectedVersion", "operationReason");
|
||||
assertThat(paths)
|
||||
.doesNotContainKey("/api/platform/v1/first-leg/loading-plans/{planId}/complete");
|
||||
assertThat(stringMap(schemas.get("LongId"))).containsEntry("type", "string");
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class UnifiedInboundOpenApiContractTest {
|
||||
@Test
|
||||
void documentsInternalUnifiedOrderEndpointsAndLegacyWaitSplitMapping() throws Exception {
|
||||
void documentsInternalUnifiedOrderEndpointsAndSevenStatuses() throws Exception {
|
||||
String yaml = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Map<String, Object> document = stringMap(new Yaml().load(yaml));
|
||||
@@ -25,13 +25,13 @@ class UnifiedInboundOpenApiContractTest {
|
||||
Map<String, Object> schemas = stringMap(stringMap(document.get("components")).get("schemas"));
|
||||
Map<String, Object> responseStatus = stringMap(schemas.get("UnifiedMainStatus"));
|
||||
assertThat(stringList(responseStatus.get("enum"))).containsExactly(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED");
|
||||
|
||||
Map<String, Object> query = stringMap(schemas.get("UnifiedInboundQuery"));
|
||||
Map<String, Object> queryStatus = stringMap(stringMap(query.get("properties")).get("mainStatus"));
|
||||
assertThat(stringList(queryStatus.get("enum"))).contains("WAIT_SPLIT");
|
||||
assertThat(queryStatus.get("description").toString()).contains("INBOUNDED");
|
||||
assertThat(queryStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED", "独立");
|
||||
|
||||
Map<String, Object> inbound = stringMap(schemas.get("UnifiedInboundOrderSummary"));
|
||||
assertThat(stringMap(inbound.get("properties"))).containsKeys(
|
||||
|
||||
+12
-8
@@ -1,7 +1,7 @@
|
||||
package com.baiyue.firstleg.customs.service;
|
||||
|
||||
import com.baiyue.firstleg.customs.entity.CustomsCaseEntity;
|
||||
import com.baiyue.firstleg.customs.repository.CustomsRepository;
|
||||
import com.baiyue.firstleg.entity.ContainerDocumentEntity;
|
||||
import com.baiyue.firstleg.repository.LoadingListRepository;
|
||||
import com.baiyue.firstleg.outbound.dispatch.gate.OutboundDispatchGateContributor.Context;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -11,7 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class CustomsDispatchGateContributorTest {
|
||||
@Mock CustomsRepository repository;
|
||||
@Mock LoadingListRepository repository;
|
||||
@InjectMocks CustomsDispatchGateContributor contributor;
|
||||
|
||||
@BeforeEach void setUp() { MockitoAnnotations.openMocks(this); }
|
||||
@@ -22,12 +22,16 @@ class CustomsDispatchGateContributorTest {
|
||||
.isEqualTo("INVALID_CUSTOMER_SCOPE");
|
||||
}
|
||||
|
||||
@Test void passesOnlyReleasedTerminalState() {
|
||||
CustomsCaseEntity value = new CustomsCaseEntity(); value.setStatus("RECEIPT_AVAILABLE");
|
||||
when(repository.gateCase(10001L, 21L, 31L, 81L)).thenReturn(value);
|
||||
@Test void passesOnlyCompletedSimpleDeclaration() {
|
||||
ContainerDocumentEntity value = new ContainerDocumentEntity();
|
||||
when(repository.document(81L, "CUSTOMS_DECLARATION")).thenReturn(value);
|
||||
assertThat(contributor.assess(context()).passed()).isFalse();
|
||||
value.setStatus("CLEARANCE_COMPLETED");
|
||||
assertThat(contributor.assess(context()).passed()).isTrue();
|
||||
value.setCompletedAt(java.time.LocalDateTime.now());
|
||||
assertThat(contributor.assess(context()))
|
||||
.satisfies(item -> {
|
||||
assertThat(item.passed()).isTrue();
|
||||
assertThat(item.requirement()).isEqualTo("COMPLETED");
|
||||
});
|
||||
}
|
||||
|
||||
private static Context context() { return new Context(10001L, 10002L, 21L, 31L, 81L, 91L, null); }
|
||||
|
||||
+3
-7
@@ -7,7 +7,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LoadingCompletionGateSqlContractTest {
|
||||
@Test
|
||||
void requiresEveryLinkedContainerToPassBookingTruckAndCustomsReceiptGates() throws Exception {
|
||||
void requiresEveryLinkedContainerToPassBookingAndTruckGates() throws Exception {
|
||||
Select select = LoadingCompletionGateMapper.class
|
||||
.getDeclaredMethod("snapshot", long.class, long.class).getAnnotation(Select.class);
|
||||
String sql = String.join(" ", select.value()).replaceAll("\\s+", " ");
|
||||
@@ -19,13 +19,9 @@ class LoadingCompletionGateSqlContractTest {
|
||||
.contains("b.status='RELEASED'")
|
||||
.contains("fl_truck_appointment")
|
||||
.contains("t.status='COMPLETED'")
|
||||
.contains("fl_customs_case")
|
||||
.contains("fl_customs_receipt")
|
||||
.contains("c.status='CLEARANCE_COMPLETED'")
|
||||
.contains("r.receipt_type='RELEASED'")
|
||||
.contains("r.file_id IS NOT NULL")
|
||||
.contains("newer.receipt_version>r.receipt_version")
|
||||
.contains("l.tenant_id=#{tenantId} AND l.inbound_id=#{inboundId}")
|
||||
.doesNotContain("truckReady, FROM")
|
||||
.doesNotContain("fl_customs_case", "fl_customs_receipt", "customsReady")
|
||||
.doesNotContain("<", ">");
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -79,7 +79,7 @@ class LoadingDeliveryServiceTest {
|
||||
LoadingActionResponse result = service.completeLoading(10, 20, 7, "admin", "k1", new CompleteLoadingRequest(3, null));
|
||||
assertThat(result.mainStatus()).isEqualTo("LOADED");
|
||||
verify(loadingModelBridge).requireCompleted(10, 20);
|
||||
verify(completionGate).requireBookingTransportAndCustoms(10, 20);
|
||||
verify(completionGate).requireBookingAndTransport(10, 20);
|
||||
verify(histories).insert(any(InboundStatusHistoryEntity.class));
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ class LoadingDeliveryServiceTest {
|
||||
|
||||
assertThat(result.mainStatus()).isEqualTo("LOADED");
|
||||
verify(loadingModelBridge).requireCompleted(10, 20);
|
||||
verify(completionGate).requireBookingTransportAndCustoms(10, 20);
|
||||
verify(completionGate).requireBookingAndTransport(10, 20);
|
||||
verify(repository).completeLoadedChild(10, 20, 7, 3);
|
||||
verify(repository).aggregateRootLoaded(10, 20, 7);
|
||||
verify(histories).insert(argThat((InboundStatusHistoryEntity history) ->
|
||||
@@ -172,7 +172,7 @@ class LoadingDeliveryServiceTest {
|
||||
verify(repository).completeLoadedChild(10, 20, 7, 5);
|
||||
verify(repository).aggregateRootLoaded(10, 10, 7);
|
||||
verify(loadingModelBridge).requireCompleted(10, 20);
|
||||
verify(completionGate).requireBookingTransportAndCustoms(10, 20);
|
||||
verify(completionGate).requireBookingAndTransport(10, 20);
|
||||
verify(histories).insert(any(InboundStatusHistoryEntity.class));
|
||||
verify(repository).completeOperation(10L, 20L, 7L, "COMPLETE_LOADING", "slice-replay", resultJson.get());
|
||||
}
|
||||
|
||||
+2
@@ -44,6 +44,8 @@ class ContainerBookingMapperContractTest {
|
||||
.contains("capacity_cbm")
|
||||
.contains("p.plan_status!='EDITING'")
|
||||
.contains("p.plan_status='COMPLETED'")
|
||||
.contains("#{containerNo} IS NOT NULL")
|
||||
.contains("fl_loading_pick_task")
|
||||
.contains("version=#{expectedVersion}")
|
||||
.doesNotContain("<")
|
||||
.doesNotContain(">");
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InboundOrderStatusSqlContractTest {
|
||||
@Test
|
||||
void advancesThroughPricingBeforeOpeningWaitSplit() {
|
||||
assertThat(sql("approveMeasurement"))
|
||||
.contains("process_status = 'WAIT_PRICING'")
|
||||
.doesNotContain("inbounded_at =");
|
||||
assertThat(sql("completeInbound"))
|
||||
.contains("process_status = 'WAIT_SPLIT'")
|
||||
.contains("inbounded_at = CURRENT_TIMESTAMP(3)")
|
||||
.contains("process_status = 'INBOUNDING'");
|
||||
}
|
||||
|
||||
private static String sql(String methodName) {
|
||||
Method method = Arrays.stream(InboundOrderMapper.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals(methodName))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
return String.join(" ", method.getAnnotation(Update.class).value());
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -18,11 +18,13 @@ class InboundStatusUnificationSqlContractTest {
|
||||
|
||||
assertThat(module)
|
||||
.contains("CREATE PROCEDURE assert_inbounded_compatibility()")
|
||||
.contains("二、三期对外统一使用 INBOUNDED")
|
||||
.contains("新完成入库记录使用 WAIT_SPLIT")
|
||||
.contains("存量 INBOUNDED")
|
||||
.doesNotContain("UPDATE fl_inbound_order")
|
||||
.doesNotContain("UPDATE fl_inbound_status_history");
|
||||
assertThat(unified)
|
||||
.contains("二、三期对外统一使用 INBOUNDED")
|
||||
.contains("新完成入库记录使用 WAIT_SPLIT")
|
||||
.contains("存量 INBOUNDED")
|
||||
.doesNotContain("SET process_status = 'INBOUNDED'")
|
||||
.doesNotContain("SET from_status = 'INBOUNDED'")
|
||||
.doesNotContain("SET to_status = 'INBOUNDED'");
|
||||
|
||||
+2
@@ -60,6 +60,8 @@ class LoadingListQueryMapperContractTest {
|
||||
.contains("version = version + 1");
|
||||
assertThat(transport)
|
||||
.contains("SET b.container_no = #{containerNo}, b.voyage_no = #{voyageNo}")
|
||||
.contains("#{containerNo} IS NOT NULL")
|
||||
.contains("fl_loading_pick_task")
|
||||
.doesNotContain("DELETE")
|
||||
.doesNotContain("fl_loading_plan_item");
|
||||
assertThat(advance)
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ class Phase3FinalUnifiedSqlContractTest {
|
||||
"-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
|
||||
"firstleg/src/main/resources/db/phase3-carrier-schedule-query-schema.sql");
|
||||
assertBlock(unified, "-- 来源:iam/src/main/resources/db/phase3-warehouse-roles.sql",
|
||||
"-- 二、三期对外统一使用 INBOUNDED;历史 WAIT_SPLIT 仅由查询适配为 INBOUNDED,统一迁移不改写主表或历史状态。",
|
||||
"-- 新完成入库记录使用 WAIT_SPLIT;存量 INBOUNDED 继续作为可分柜兼容状态,统一迁移不改写主表或历史状态。",
|
||||
"iam/src/main/resources/db/phase3-warehouse-roles.sql");
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ class SmartLoadingCandidateMapperContractTest {
|
||||
.contains("m2.status='APPROVED'")
|
||||
.contains("i.inbound_detail_id IS NOT NULL")
|
||||
.contains("o.warehouse_id=#{warehouseId}")
|
||||
.contains("o.process_status='INBOUNDED'")
|
||||
.contains("o.process_status IN ('WAIT_SPLIT','INBOUNDED')")
|
||||
.contains("SELECT SUM(li.package_count)")
|
||||
.contains("li.deleted=0")
|
||||
.contains("ORDER BY o.inbounded_at ASC, o.id ASC, i.id ASC");
|
||||
@@ -38,7 +38,7 @@ class SmartLoadingCandidateMapperContractTest {
|
||||
.contains("m.status='APPROVED'")
|
||||
.contains("SELECT MAX(m2.id)")
|
||||
.contains("o.warehouse_id=#{warehouseId}")
|
||||
.contains("o.process_status='INBOUNDED'")
|
||||
.contains("o.process_status IN ('WAIT_SPLIT','INBOUNDED')")
|
||||
.contains("SELECT SUM(li.package_count)")
|
||||
.contains("FOR UPDATE");
|
||||
}
|
||||
|
||||
+24
@@ -73,6 +73,30 @@ class SmartLoadingSqlContractTest {
|
||||
.contains("version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号'");
|
||||
}
|
||||
|
||||
@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("IF current_plan_status='COMPLETED' THEN")
|
||||
.contains("NEW.document_type NOT IN ('DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION')")
|
||||
.contains("OLD.document_type NOT IN ('DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION')");
|
||||
}
|
||||
|
||||
private static void assertSimpleCustomsDocumentContract(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("'DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', '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')")
|
||||
.contains("OLD.document_type NOT IN ('DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION')")
|
||||
.doesNotContain("OLD.document_type NOT IN ('DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION') THEN\n SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '装柜完成后货柜全部只读");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("平台菜单按钮权限使用统一前缀并授权运营角色")
|
||||
void shouldProvidePlatformPermissions() throws Exception {
|
||||
|
||||
@@ -8,8 +8,10 @@ import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerTransportHistoryMapper;
|
||||
import com.baiyue.firstleg.repository.ContainerRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
@@ -37,6 +39,7 @@ class ContainerServiceTest {
|
||||
@MockBean private ContainerRepository repository;
|
||||
@MockBean private WarehouseMasterDataClient warehouses;
|
||||
@MockBean private SmartLoadingRepository smartLoadingRepository;
|
||||
@MockBean private LoadingPickRepository loadingPickRepository;
|
||||
@MockBean private LoadingPickService loadingPickService;
|
||||
@MockBean private ContainerTransportHistoryMapper transportHistories;
|
||||
@Resource private ContainerService service;
|
||||
@@ -145,6 +148,25 @@ class ContainerServiceTest {
|
||||
org.mockito.ArgumentMatchers.eq("admin"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PDA任务创建后禁止清空真实集装箱号")
|
||||
void rejectsClearingContainerNumberAfterPdaTaskStarts() {
|
||||
ContainerBookingEntity current = booking("LOADING", 2);
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setId(31L);
|
||||
when(repository.find(11L)).thenReturn(current);
|
||||
when(smartLoadingRepository.planByBooking(11L)).thenReturn(plan);
|
||||
when(loadingPickRepository.taskByPlan(31L)).thenReturn(new LoadingPickTaskEntity());
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(11L, 9L, "admin",
|
||||
new TransportUpdateRequest(null, "V002", 2)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("不能清空真实集装箱号");
|
||||
|
||||
verify(repository, never()).updateTransport(any(Long.class), any(), any(),
|
||||
any(Integer.class), any(Long.class), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已确认货柜全部只读")
|
||||
void rejectsConfirmedUpdate() {
|
||||
|
||||
@@ -114,7 +114,7 @@ class FbaPricingServiceTest {
|
||||
void calculatesAtWaitSplitWithoutRollingBackProcessStatus() {
|
||||
readyRepository();
|
||||
InboundPricingEntity inbound = inbound("100.000", "1.0000");
|
||||
inbound.setProcessStatus("INBOUNDED");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
when(repository.findInbound(TENANT, INBOUND)).thenReturn(inbound);
|
||||
when(priceBookEstimator.estimate(any())).thenReturn(
|
||||
estimate("KG", "167.000", "2.000000", "334.00", "0.00"));
|
||||
@@ -122,8 +122,8 @@ class FbaPricingServiceTest {
|
||||
FeeSnapshotResponse response = service.calculate(TENANT, OPERATOR, INBOUND, NOW, "wait-split");
|
||||
|
||||
assertThat(response.feeStatus()).isEqualTo("CALCULATED");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
||||
verify(repository).markInboundCalculated(TENANT, INBOUND, "INBOUNDED", "KG",
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository).markInboundCalculated(TENANT, INBOUND, "WAIT_SPLIT", "KG",
|
||||
new BigDecimal("167.000"));
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ class FbaPricingServiceTest {
|
||||
@DisplayName("待分柜边界确认费用后仍保持主流程状态")
|
||||
void confirmsAtWaitSplitWithoutRollingBackProcessStatus() {
|
||||
InboundPricingEntity inbound = inbound("10.000", "1.0000");
|
||||
inbound.setProcessStatus("INBOUNDED");
|
||||
inbound.setProcessStatus("WAIT_SPLIT");
|
||||
inbound.setFeeStatus("CALCULATED");
|
||||
InboundFeeEntity fee = savedFee("CALCULATED");
|
||||
when(repository.findLatestFee(TENANT, INBOUND)).thenReturn(fee);
|
||||
@@ -148,7 +148,7 @@ class FbaPricingServiceTest {
|
||||
FeeSnapshotResponse response = service.confirm(
|
||||
TENANT, OPERATOR, INBOUND, "confirm-wait-split");
|
||||
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("INBOUNDED");
|
||||
assertThat(response.nextProcessStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository).markInboundConfirmed(TENANT, INBOUND);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -102,6 +103,23 @@ class LoadingListServiceTest {
|
||||
verify(repository, never()).updateTransport(eq(10L), eq(5), anyString(), anyString(), eq(9L), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectClearingContainerNumberAfterPdaTaskStarts() {
|
||||
AggregateRow before = row("LOADING", 3);
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
when(pickRepository.taskByPlan(10L)).thenReturn(new com.baiyue.firstleg.entity.LoadingPickTaskEntity());
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "key-1",
|
||||
new TransportUpdateRequest(null, "023E", 3, "清空测试")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("不能清空真实集装箱号");
|
||||
verify(repository, never()).updateTransport(eq(10L), eq(3),
|
||||
org.mockito.ArgumentMatchers.nullable(String.class),
|
||||
org.mockito.ArgumentMatchers.nullable(String.class), eq(9L),
|
||||
org.mockito.ArgumentMatchers.nullable(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCompleteLoadingAfterAllPdaItemsPicked() {
|
||||
AggregateRow before = row("LOADING", 2);
|
||||
@@ -206,14 +224,16 @@ class LoadingListServiceTest {
|
||||
.thenReturn(null, document(40L, LoadingListService.DESTINATION_CLEARANCE, 0, false));
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("CONTAINER_CLEARANCE_DOCUMENT"),
|
||||
eq("30"), eq(99L))).thenReturn(ApiResponse.success(new LoadingDocumentFileClient.Metadata(
|
||||
99L, 0L, "CONTAINER_CLEARANCE_DOCUMENT", "30", "clearance.pdf", "abc", "application/pdf", 20L)));
|
||||
99L, 0L, "CONTAINER_CLEARANCE_DOCUMENT", "30", "clearance.pdf", hash(), "application/pdf", 20L)));
|
||||
when(repository.createDocument(org.mockito.ArgumentMatchers.any())).thenReturn(1);
|
||||
|
||||
var result = service.attachContainerClearance(30L, 9L, "operator", "key-clearance",
|
||||
new DocumentAttachRequest(99L, 0, "上传清关资料"));
|
||||
|
||||
assertThat(result.fileName()).isEqualTo("clearance.pdf");
|
||||
verify(repository).appendHistory(eq(10L), anyString(), eq("ATTACH_DESTINATION_CLEARANCE"),
|
||||
verify(repository).appendHistory(eq(10L),
|
||||
eq("PL-" + sha256("ATTACH_DESTINATION_CLEARANCE|30|key-clearance")),
|
||||
eq("ATTACH_DESTINATION_CLEARANCE"),
|
||||
eq("LOADING"), eq("LOADING"), anyString(), anyString(), eq(9L), eq("operator"),
|
||||
eq("上传清关资料"));
|
||||
}
|
||||
@@ -241,7 +261,7 @@ class LoadingListServiceTest {
|
||||
when(repository.document(30L, LoadingListService.DESTINATION_CLEARANCE)).thenReturn(null);
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("CONTAINER_CLEARANCE_DOCUMENT"),
|
||||
eq("30"), eq(99L))).thenReturn(ApiResponse.success(new LoadingDocumentFileClient.Metadata(
|
||||
99L, 0L, "CONTAINER_CLEARANCE_DOCUMENT", "different-booking", "clearance.pdf", "abc",
|
||||
99L, 0L, "CONTAINER_CLEARANCE_DOCUMENT", "different-booking", "clearance.pdf", hash(),
|
||||
"application/pdf", 20L)));
|
||||
|
||||
assertThatThrownBy(() -> service.attachContainerClearance(30L, 9L, "operator", "key",
|
||||
@@ -306,6 +326,121 @@ 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);
|
||||
plan.setContainerBookingId(30L);
|
||||
byte[] content = "clearance-document".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
String contentHash = sha256Bytes(content);
|
||||
var file = new MockMultipartFile("file", "clearance.pdf", "application/pdf", content);
|
||||
var metadata = new LoadingDocumentFileClient.Metadata(99L, 0L,
|
||||
"CONTAINER_CLEARANCE_DOCUMENT", "30", "clearance.pdf", contentHash, "application/pdf",
|
||||
(long) content.length);
|
||||
when(repository.aggregateByBooking(30L)).thenReturn(plan);
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
ContainerDocumentEntity saved = document(40L, LoadingListService.DESTINATION_CLEARANCE, 0, false);
|
||||
saved.setFileHash(contentHash);
|
||||
when(repository.document(30L, LoadingListService.DESTINATION_CLEARANCE)).thenReturn(null, saved);
|
||||
when(documentFiles.upload(anyString(), eq(9L), eq(0L), eq("CONTAINER_CLEARANCE_DOCUMENT"),
|
||||
eq("30"), eq(file))).thenReturn(ApiResponse.success(metadata));
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("CONTAINER_CLEARANCE_DOCUMENT"),
|
||||
eq("30"), eq(99L))).thenReturn(ApiResponse.success(metadata));
|
||||
when(repository.createDocument(org.mockito.ArgumentMatchers.any())).thenReturn(1);
|
||||
|
||||
var result = service.uploadContainerClearance(
|
||||
30L, 9L, "operator", "upload-key", 0, "直接上传", file);
|
||||
|
||||
assertThat(result.fileHash()).isEqualTo(contentHash);
|
||||
verify(documentFiles).upload(anyString(), eq(9L), eq(0L), eq("CONTAINER_CLEARANCE_DOCUMENT"),
|
||||
eq("30"), eq(file));
|
||||
verify(repository).appendHistory(eq(10L),
|
||||
eq("PL-" + sha256("UPLOAD_DESTINATION_CLEARANCE|30|upload-key")),
|
||||
eq("UPLOAD_DESTINATION_CLEARANCE"), eq("LOADING"), eq("LOADING"), anyString(),
|
||||
anyString(), eq(9L), eq("operator"), eq("直接上传"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipartReplayDoesNotUploadFileAgain() {
|
||||
AggregateRow plan = row("LOADING", 2);
|
||||
plan.setContainerBookingId(30L);
|
||||
byte[] content = "clearance-document".getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
var file = new MockMultipartFile("file", "clearance.pdf", "application/pdf", content);
|
||||
String requestHash = sha256("UPLOAD|DESTINATION_CLEARANCE|30|0|null|" + sha256Bytes(content));
|
||||
HistoryRow replay = new HistoryRow();
|
||||
replay.setAfterSnapshot("{\"requestHash\":\"" + requestHash + "\"}");
|
||||
when(repository.aggregateByBooking(30L)).thenReturn(plan);
|
||||
when(repository.history(anyString())).thenReturn(replay);
|
||||
when(repository.document(30L, LoadingListService.DESTINATION_CLEARANCE))
|
||||
.thenReturn(document(40L, LoadingListService.DESTINATION_CLEARANCE, 0, false));
|
||||
|
||||
service.uploadContainerClearance(30L, 9L, "operator", "upload-key", 0, null, file);
|
||||
|
||||
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_DOCUMENT", "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_DOCUMENT"),
|
||||
eq("10"), eq(file))).thenReturn(ApiResponse.success(metadata));
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DOCUMENT"),
|
||||
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_DOCUMENT"),
|
||||
eq("10"), eq(file));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReusedIdempotencyKeyWithDifferentAttachRequest() {
|
||||
AggregateRow plan = row("LOADING", 2);
|
||||
plan.setContainerBookingId(30L);
|
||||
HistoryRow replay = new HistoryRow();
|
||||
replay.setAfterSnapshot("{\"requestHash\":\"different\"}");
|
||||
when(repository.aggregateByBooking(30L)).thenReturn(plan);
|
||||
when(repository.history(anyString())).thenReturn(replay);
|
||||
|
||||
assertThatThrownBy(() -> service.attachContainerClearance(30L, 9L, "operator", "reused-key",
|
||||
new DocumentAttachRequest(100L, 0, null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.CONFLICT);
|
||||
verify(repository, never()).createDocument(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
private static AggregateRow row(String status, int version) {
|
||||
AggregateRow row = new AggregateRow();
|
||||
row.setPlanId(10L);
|
||||
@@ -319,10 +454,31 @@ class LoadingListServiceTest {
|
||||
private static ContainerDocumentEntity document(long id, String type, int version, boolean completed) {
|
||||
ContainerDocumentEntity value = new ContainerDocumentEntity();
|
||||
value.setId(id); value.setContainerBookingId(30L); value.setDocumentType(type);
|
||||
value.setFileId(99L); value.setFileName("clearance.pdf"); value.setFileHash("abc");
|
||||
value.setFileId(99L); value.setFileName("clearance.pdf"); value.setFileHash(hash());
|
||||
value.setUploadedBy(9L); value.setUploadedByName("operator"); value.setVersion(version);
|
||||
value.setCreatedAt(java.time.LocalDateTime.now());
|
||||
if (completed) value.setCompletedAt(java.time.LocalDateTime.now());
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String hash() { return "a".repeat(64); }
|
||||
|
||||
private static String sha256Bytes(byte[] value) {
|
||||
try {
|
||||
return java.util.HexFormat.of().formatHex(
|
||||
java.security.MessageDigest.getInstance("SHA-256").digest(value));
|
||||
} catch (java.security.NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static String sha256(String value) {
|
||||
try {
|
||||
byte[] digest = java.security.MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
return java.util.HexFormat.of().formatHex(digest);
|
||||
} catch (java.security.NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ class MeasurementReviewServiceTest {
|
||||
ArgumentCaptor.forClass(InboundStatusHistoryEntity.class);
|
||||
verify(fixture.repository).insertHistory(history.capture());
|
||||
assertThat(history.getValue().getFromStatus()).isEqualTo("MEASURE_PENDING");
|
||||
assertThat(history.getValue().getToStatus()).isEqualTo("INBOUNDED");
|
||||
assertThat(history.getValue().getToStatus()).isEqualTo("WAIT_PRICING");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+8
-8
@@ -28,10 +28,10 @@ class UnifiedInboundQueryServiceTest {
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("MEASURING")).isEqualTo("WAIT_MEASURE");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("MEASURE_PENDING")).isEqualTo("WAIT_MEASURE");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("MEASURE_REJECTED")).isEqualTo("WAIT_MEASURE");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_PRICING")).isEqualTo("INBOUNDED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("INBOUNDED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("INBOUNDED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_SPLIT")).isEqualTo("INBOUNDED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_PRICING")).isEqualTo("WAIT_MEASURE");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("WAIT_MEASURE");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("WAIT_MEASURE");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_SPLIT")).isEqualTo("WAIT_SPLIT");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDED")).isEqualTo("INBOUNDED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("LOADING")).isEqualTo("LOADING");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("LOADED")).isEqualTo("LOADED");
|
||||
@@ -50,21 +50,21 @@ class UnifiedInboundQueryServiceTest {
|
||||
.isEqualTo("CONFIRM_PRELOAD");
|
||||
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDING", null)).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "CONFIRMED")).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "WAIT_SPLIT", null))
|
||||
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", null))
|
||||
.isEqualTo("SMART_SPLIT");
|
||||
assertThat(UnifiedInboundQueryService.nextAction("LOADED", "LOADED", null)).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("DELIVERED", "DELIVERED", null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void inboundedFilterIncludesHistoricalWaitSplitRows() throws Exception {
|
||||
void inboundedAndWaitSplitFiltersRemainDistinct() throws Exception {
|
||||
var method = UnifiedInboundQueryService.class.getDeclaredMethod("internalStatuses", String.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertThat(method.invoke(new UnifiedInboundQueryService(), "INBOUNDED"))
|
||||
.isEqualTo(java.util.List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "INBOUNDED", "WAIT_SPLIT"));
|
||||
.isEqualTo(java.util.List.of("INBOUNDED"));
|
||||
assertThat(method.invoke(new UnifiedInboundQueryService(), "WAIT_SPLIT"))
|
||||
.isEqualTo(java.util.List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "INBOUNDED", "WAIT_SPLIT"));
|
||||
.isEqualTo(java.util.List.of("WAIT_SPLIT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+3
-3
@@ -47,19 +47,19 @@ class SplitPreloadServiceTest {
|
||||
@Test
|
||||
void smartSplitCreatesEditablePendingPlan() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "INBOUNDED", 3));
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "WAIT_SPLIT", 3));
|
||||
when(repository.findPlan(10, 20)).thenReturn(null);
|
||||
SmartSplitRequest request = new SmartSplitRequest(0,
|
||||
List.of(new PreloadItem("C1", "A", 2, BigDecimal.ONE, null)), "r");
|
||||
SplitActionResponse result = service.smartSplit(10, 20, 7, "admin", "k2", request);
|
||||
assertThat(result.mainStatus()).isEqualTo("INBOUNDED");
|
||||
assertThat(result.mainStatus()).isEqualTo("WAIT_SPLIT");
|
||||
verify(repository).createPlan(any(PreloadPlanEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmsPlanAndMovesInboundToLoading() {
|
||||
when(repository.claimOperation(anyLong(), anyLong(), anyLong(), anyString(), anyString())).thenReturn(true);
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "INBOUNDED", 4));
|
||||
when(repository.findInbound(10, 20)).thenReturn(new InboundSnapshot(20L, 10L, "WAIT_SPLIT", 4));
|
||||
PreloadPlanEntity plan = new PreloadPlanEntity();
|
||||
plan.setTenantId(10L); plan.setInboundId(20L); plan.setStatus("CONFIRMING"); plan.setVersion(1);
|
||||
plan.setItemsJson("[]");
|
||||
|
||||
Reference in New Issue
Block a user