This commit is contained in:
2026-08-14 16:06:27 +08:00
parent 87eb1aa72f
commit 87e919f3c4
54 changed files with 984 additions and 175 deletions
@@ -31,5 +31,18 @@ class PdaConvergenceOpenApiContractTest {
.contains("changing it never resynchronizes the task or item versions")
.contains("expectedTaskVersion")
.contains("CompleteLoadingRequest");
String taskLookup = yaml.substring(
yaml.indexOf(" /api/platform/v1/first-leg/pda-loading/tasks:"),
yaml.indexOf(" /api/platform/v1/first-leg/pda-loading/tasks/pending:"));
String batchRequest = yaml.substring(
yaml.indexOf(" BatchRequest:"),
yaml.indexOf(" BatchItemRequest:"));
assertThat(taskLookup)
.contains("name: cabinetNo", "name: loadingPlanId")
.doesNotContain("name: containerNo");
assertThat(batchRequest)
.contains("loadingPlanId:", "containerBookingId:")
.doesNotContain("containerNo:");
}
}
@@ -12,7 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class PlatformLoadingPdaControllerContractTest {
@Test
void exposesAssignmentWorkAndReviewWithDedicatedAuthorities() throws Exception {
authority("task", "platform:firstleg:pda:view", AuthenticatedUser.class, String.class, String.class, Long.class, long.class);
authority("task", "platform:firstleg:pda:view", AuthenticatedUser.class, String.class, Long.class, long.class);
authority("pendingTasks", "platform:firstleg:pda:view", AuthenticatedUser.class, String.class);
authority("inventorySnapshot", "platform:firstleg:pda:view", AuthenticatedUser.class, long.class, long.class);
authority("claim", "platform:firstleg:pda:work", AuthenticatedUser.class, long.class, ClaimRequest.class);
@@ -16,6 +16,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.groups.Tuple.tuple;
@DisplayName("船期价格表公共契约")
class PriceBookContractTest {
@@ -24,11 +25,23 @@ class PriceBookContractTest {
void keepsStableChannelCatalog() {
assertThat(PriceBookChannelCatalog.CHANNELS).hasSize(15);
assertThat(PriceBookChannelCatalog.CHANNELS)
.extracting(value -> value.channelCode() + ":" + value.inboundCodePrefix())
.contains("MATSON_CLX_TRUCK:ZBYHK", "OA_LA_FULL_TRUCK:PCKK", "OA_CHICAGO_RAIL:CPHT");
assertThat(PriceBookChannelCatalog.CHANNELS)
.filteredOn(value -> "OA_NEW_YORK_TRUCK".equals(value.channelCode()))
.singleElement().extracting(value -> value.overseasInboundCodePrefix()).isEqualTo("NPHWK");
.extracting("channelCode", "inboundCodePrefix", "overseasInboundCodePrefix")
.containsExactly(
tuple("MATSON_CLX_TRUCK", "ZBYHK", "ZBYHWK"),
tuple("MATSON_MAX_TRUCK", "BYHK", "BYHWK"),
tuple("EXX_TRUCK", "EXXHK", "EXXHWK"),
tuple("HEDE_TRUCK", "HDHK", "HDHWK"),
tuple("EXX_RAIL_NY", "NYEXX", null),
tuple("OA_LA_TRUCK", "PCHK", "PCHWK"),
tuple("OA_LA_FULL_TRUCK", "PCKK", null),
tuple("OA_CHICAGO_RAIL", "CPHT", null),
tuple("OA_HOUSTON_TRUCK", "HPHK", null),
tuple("OA_NEW_YORK_TRUCK", "NPHK", "NPHWK"),
tuple("OA_SAVANNAH_TRUCK", "SPHK", null),
tuple("MATSON_CLX_PARCEL", "ZBYHP", null),
tuple("MATSON_MAX_PARCEL", "BYHP", null),
tuple("HEDE_PARCEL", "HDHP", null),
tuple("OA_PARCEL", "PCHP", null));
assertThat(PriceBookChannelCatalog.CHANNELS.stream()
.map(PriceBookChannelCatalog.ChannelDefinition::channelCode))
.containsExactly(
@@ -102,5 +115,12 @@ class PriceBookContractTest {
.contains("/api/platform/v1/fba-pricing/carrier-rates/price-books/{versionId}/source")
.contains("确认并立即生效")
.contains("SUPERSEDED");
String firstlegOpenapi = new ClassPathResource("openapi/firstleg-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(firstlegOpenapi)
.contains("/internal/firstleg/v1/pricing/channels/validate:")
.contains("required: [tenantId, originWarehouseId, channelCode, destinationType]")
.contains("同一渠道的包税与不包税订单共用编码前缀");
}
}
@@ -5,6 +5,8 @@ import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ActionRequest;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ChangeRequest;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Detail;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.AllocationRequest;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ReleaseRequest;
import com.baiyue.firstleg.service.LoadingPickService;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
@@ -30,6 +32,7 @@ import static org.mockito.Mockito.*;
class OceanBookingServiceTest {
@Mock private OceanBookingRepository repository;
@Mock private LoadingPickService loadingPickService;
@InjectMocks private OceanBookingService service;
@BeforeEach
@@ -77,6 +80,24 @@ class OceanBookingServiceTest {
verify(repository, never()).transition(anyLong(), anyString(), anyString(), any(), any(), anyInt(), anyLong(), anyString());
}
@Test
void opensAllocatedContainerPdaTasksAfterRelease() {
OceanBookingEntity confirmed = booking("CONFIRMED", 3);
OceanBookingEntity released = booking("RELEASED", 4);
var allocation = new OceanBookingContainerAllocationEntity();
allocation.setContainerBookingId(12L);
when(repository.find(1L)).thenReturn(confirmed, released);
when(repository.release(eq(1L), any(ReleaseRequest.class), eq(9L), eq("admin"))).thenReturn(1);
when(repository.allocations(1L)).thenReturn(List.of(allocation));
var request = new ReleaseRequest(3, "BL-001", "VESSEL", "V001",
LocalDateTime.of(2026, 9, 1, 0, 0), LocalDateTime.of(2026, 9, 20, 0, 0), "放舱");
Detail result = service.release(1L, 9L, "admin", "release-1", request);
assertThat(result.oceanBooking().getStatus()).isEqualTo("RELEASED");
verify(loadingPickService).beginIfReady(12L, 9L, "admin");
}
@Test
void keepsCurrentVoyagePendingUntilChangeConfirmation() {
OceanBookingEntity current = booking("CONFIRMED", 5);
@@ -143,9 +143,7 @@ class ContainerServiceTest {
service.updateTransport(11L, 9L, "admin",
new TransportUpdateRequest("MSCU1234567", "V002", 2));
verify(loadingPickService).begin(org.mockito.ArgumentMatchers.eq(31L),
org.mockito.ArgumentMatchers.eq("MSCU1234567"), org.mockito.ArgumentMatchers.eq(9L),
org.mockito.ArgumentMatchers.eq("admin"), any());
verify(loadingPickService).beginIfReady(11L, 9L, "admin");
}
@Test
@@ -165,8 +163,7 @@ class ContainerServiceTest {
service.updateTransport(11L, 9L, "admin",
new TransportUpdateRequest("MSCU7654321", "V002", 2));
verify(loadingPickService, never()).begin(anyLong(), any(), anyLong(), any(), any());
verify(smartLoadingRepository, never()).items(31L);
verify(loadingPickService).beginIfReady(11L, 9L, "admin");
}
@Test
@@ -85,6 +85,28 @@ class InboundMeasurementServiceTest {
assertThat(revision.getValue().getAfterWeightValue()).isEqualByComparingTo("13.200");
}
@Test
void correctingConfirmedWeightImmediatelyRemovesItFromInboundTotal() {
InboundOrderEntity inbound = inbound("MEASURING");
inbound.setActualGrossWeightKg(new BigDecimal("12.500"));
InboundWeightResultEntity weight = weight(20L, "12.500", "KG", "CONFIRMED", 4);
weight.setFinalWeightKg(new BigDecimal("12.500"));
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
when(repository.updateWeight(weight)).thenReturn(true);
when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight));
when(repository.updateInboundWeightSummary(inbound)).thenReturn(true);
service.correctWeightResult(1L, 10L, 20L, 9L, "操作员",
new InboundMeasurementRequests.CorrectWeightResultRequest(
4, new BigDecimal("13.200"), "KG", "秤面显示修正"));
assertThat(weight.getRecognitionStatus()).isEqualTo("PENDING_CONFIRM");
assertThat(weight.getFinalWeightKg()).isNull();
assertThat(inbound.getActualGrossWeightKg()).isNull();
verify(repository).updateInboundWeightSummary(inbound);
}
@Test
void detailReturnsStandardPalletRecommendationWithoutAllocatingPallet() {
InboundOrderEntity inbound = inbound("MEASURE_PENDING");
@@ -19,6 +19,9 @@ import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
@@ -46,6 +49,8 @@ class LoadingPickServiceTest {
@MockBean
private SmartLoadingRepository smartLoadingRepository;
@MockBean
private OceanBookingRepository oceanBookingRepository;
@MockBean
private LoadingListService loadingListService;
@MockBean
private PdaPickSettlementService settlementService;
@@ -54,6 +59,51 @@ class LoadingPickServiceTest {
@Resource
private LoadingPickService service;
@Test
void opensTaskOnlyAfterReleasedBookingAndRealContainerNumber() {
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
booking.setId(40L); booking.setContainerNo("MSCU-001");
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
plan.setId(10L); plan.setContainerBookingId(40L); plan.setPlanStatus("EDITING");
plan.setPreloadPushedAt(java.time.LocalDateTime.now());
var allocation = new OceanBookingContainerAllocationEntity(); allocation.setOceanBookingId(90L);
var ocean = new OceanBookingEntity(); ocean.setId(90L); ocean.setStatus("RELEASED");
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
when(smartLoadingRepository.plan(10L)).thenReturn(plan);
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
when(repository.taskByPlan(10L)).thenReturn(null);
when(smartLoadingRepository.items(10L)).thenReturn(List.of());
doAnswer(invocation -> {
invocation.getArgument(0, LoadingPickTaskEntity.class).setId(20L);
return null;
}).when(repository).insert(any(LoadingPickTaskEntity.class));
service.beginIfReady(40L, 7L, "admin");
verify(repository).insert(any(LoadingPickTaskEntity.class));
}
@Test
void doesNotOpenTaskBeforeOceanRelease() {
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
booking.setId(40L); booking.setContainerNo("MSCU-001");
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
plan.setId(10L); plan.setContainerBookingId(40L); plan.setPlanStatus("EDITING");
plan.setPreloadPushedAt(java.time.LocalDateTime.now());
var allocation = new OceanBookingContainerAllocationEntity(); allocation.setOceanBookingId(90L);
var ocean = new OceanBookingEntity(); ocean.setId(90L); ocean.setStatus("CONFIRMED");
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
service.beginIfReady(40L, 7L, "admin");
verify(repository, never()).insert(any(LoadingPickTaskEntity.class));
}
@Test
void stableCabinetAndPlanQueriesReturnSameTaskWithCurrentContainerNumber() {
LoadingPickTaskEntity task = task();
@@ -66,8 +116,8 @@ class LoadingPickServiceTest {
when(repository.items(20L, 101L)).thenReturn(List.of());
when(repository.cartonLabels(20L)).thenReturn(List.of());
var byCabinet = service.task(null, "CAB-001", null, 101L);
var byPlan = service.task(null, null, 10L, 101L);
var byCabinet = service.task("CAB-001", null, 101L);
var byPlan = service.task(null, 10L, 101L);
assertThat(byCabinet.id()).isEqualTo(20L);
assertThat(byPlan.id()).isEqualTo(20L);
@@ -76,27 +126,12 @@ class LoadingPickServiceTest {
@Test
void rejectsMissingOrConflictingCompatibilitySelectors() {
assertThatThrownBy(() -> service.task(null, null, null, 101L))
assertThatThrownBy(() -> service.task(null, null, 101L))
.isInstanceOf(BizException.class).hasMessageContaining("必须且只能提供一个");
assertThatThrownBy(() -> service.task("MSCU", "CAB", null, 101L))
assertThatThrownBy(() -> service.task("CAB", 10L, 101L))
.isInstanceOf(BizException.class).hasMessageContaining("必须且只能提供一个");
}
@Test
void legacyContainerLookupReturnsCurrentTransportDisplayWithoutChangingIdentity() {
LoadingPickTaskEntity task = task(); task.setContainerBookingId(40L); task.setCabinetNo("CAB-001");
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
booking.setId(40L); booking.setContainerNo("MSCU-CURRENT");
when(repository.taskByContainer("MSCU-CURRENT", 101L)).thenReturn(task);
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
when(repository.items(20L, 101L)).thenReturn(List.of());
when(repository.cartonLabels(20L)).thenReturn(List.of());
var result = service.task("MSCU-CURRENT", null, null, 101L);
assertThat(result.id()).isEqualTo(20L);
assertThat(result.loadingPlanId()).isEqualTo(10L);
assertThat(result.currentContainerNo()).isEqualTo("MSCU-CURRENT");
}
@Test
void keepsPlanLoadingWhenItemsRemain() {
LoadingPickTaskEntity task = task();
@@ -271,7 +306,7 @@ class LoadingPickServiceTest {
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
String snapshotToken = service.inventorySnapshot(20L, 101L).snapshotToken();
BatchRequest request = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 0, snapshotToken, List.of(
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, snapshotToken, List.of(
new BatchItemRequest(30L, "PICK", 2, null, "MANUAL", List.of(), 0, null),
new BatchItemRequest(31L, "SKIP", null, null, null, null, 0, "damaged carton")));
@@ -301,7 +336,7 @@ class LoadingPickServiceTest {
when(repository.assignment(20L)).thenReturn(assignment());
when(repository.items(20L)).thenReturn(items);
when(repository.items(20L, 101L)).thenReturn(items);
BatchRequest request = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 0, "stale-token", List.of(
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "stale-token", List.of(
new BatchItemRequest(30L, "PICK", 2, null, "MANUAL", List.of(), 0, null),
new BatchItemRequest(31L, "PARTIAL", null, 4, "AUTO", null, 0, null)));
@@ -325,7 +360,7 @@ class LoadingPickServiceTest {
when(warehouse.occupiedResources(any(), any(Long.class), any(), any(Long.class))).thenReturn(
ApiResponse.success(List.of(new WarehousePalletLocationClient.OccupiedResource(
71L, 72L, 0, 101L, 73L, 74L, "A-01", false, 2))));
BatchRequest request = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 0, "0".repeat(64), List.of(
BatchRequest request = new BatchRequest(101L, 10L, 99L, 0, "0".repeat(64), List.of(
new BatchItemRequest(30L, "PICK", 2, null, "MANUAL", List.of(), 0, null)));
assertThatThrownBy(() -> service.batch(20L, "batch-inventory-stale", request, 9L, "operator"))
@@ -343,7 +378,7 @@ class LoadingPickServiceTest {
LoadingPickTaskEntity task = task();
task.setVersion(3);
when(repository.taskForUpdate(20L)).thenReturn(task);
BatchRequest request = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 2, "stale-token", List.of(
BatchRequest request = new BatchRequest(101L, 10L, 99L, 2, "stale-token", List.of(
new BatchItemRequest(30L, "PICK", 2, null, "AUTO", null, 0, null)));
assertThatThrownBy(() -> service.batch(20L, "batch-stale", request, 9L, "operator"))
@@ -373,14 +408,14 @@ class LoadingPickServiceTest {
30L, "PICK", 2, null, "MANUAL", List.of(), 0, null);
assertThatThrownBy(() -> service.batch(20L, "batch-stale", new BatchRequest(
101L, 10L, 99L, "CSLU1234567", 1, snapshotToken, List.of(line)), 9L, "operator"))
101L, 10L, 99L, 1, snapshotToken, List.of(line)), 9L, "operator"))
.isInstanceOf(BizException.class)
.hasMessageContaining("任务版本已变化");
assertThat(item.getVersion()).isZero();
assertThat(item.getPickedPackageCount()).isZero();
service.batch(20L, "batch-valid", new BatchRequest(
101L, 10L, 99L, "CSLU1234567", 0, snapshotToken, List.of(line)), 9L, "operator");
101L, 10L, 99L, 0, snapshotToken, List.of(line)), 9L, "operator");
verify(repository, times(1)).pick(30L, 2, "MANUAL", "[]", 0, 101L);
verify(repository, times(1)).touchTask(20L, 0);
@@ -401,7 +436,7 @@ class LoadingPickServiceTest {
service.begin(10L, "CSLU1234567", 9L, "operator", List.of(planItem));
BatchRequest staleRequest = new BatchRequest(101L, 10L, 99L, "CSLU1234567", 1,
BatchRequest staleRequest = new BatchRequest(101L, 10L, 99L, 1,
"stale-token", List.of(
new BatchItemRequest(30L, "PICK", 2, null, "AUTO", null, 0, null)));
assertThatThrownBy(() -> service.batch(20L, "batch-stale-after-resync", staleRequest,
@@ -389,7 +389,7 @@ class SmartLoadingServiceTest {
assertThat(error.getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
assertThat(error.getMessage()).contains("重量").contains("110%");
verify(repository, never()).push(anyLong(), anyString(), anyInt(), anyLong(), anyString());
verify(loadingPickService, never()).begin(anyLong(), any(), anyLong(), anyString(), anyList());
verify(loadingPickService, never()).beginIfReady(anyLong(), anyLong(), anyString());
}
@Test
@@ -407,7 +407,7 @@ class SmartLoadingServiceTest {
assertThat(result.status()).isEqualTo("EDITING");
verify(repository).push(1, "no-container-key", 1, 7, "u");
verify(loadingPickService, never()).begin(anyLong(), any(), anyLong(), anyString(), anyList());
verify(loadingPickService).beginIfReady(99L, 7L, "u");
}
private void stubAssignment() {