This commit is contained in:
2026-09-15 13:36:58 +08:00
parent 1bb69698de
commit f9649c44e9
44 changed files with 647 additions and 63 deletions
@@ -24,8 +24,10 @@ class TailPlanCommandOpenApiContractTest {
int manualSchemaEnd = api.indexOf("\n PageMetadata:", manualSchemaStart);
assertTrue(manualSchemaStart >= 0 && manualSchemaEnd > manualSchemaStart);
assertFalse(api.substring(manualSchemaStart, manualSchemaEnd).contains("inboundCode"));
assertFalse(api.substring(manualSchemaStart, manualSchemaEnd).contains("plannedArrivalDate"));
assertFalse(java.util.Arrays.stream(ManualInboundPlanRequest.class.getRecordComponents())
.anyMatch(component -> "inboundCode".equals(component.getName())));
.anyMatch(component -> java.util.Set.of("inboundCode", "plannedArrivalDate")
.contains(component.getName())));
assertTrue(api.contains("/api/platform/v1/last-leg/orders/{id}/hold:"));
assertFalse(api.contains("/api/platform/v1/last-leg/orders/{id}/detained:"));
assertTrue(api.contains("/api/platform/v1/last-leg/orders/{id}/manual-nodes:"));
@@ -20,7 +20,6 @@ import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Set;
import java.time.LocalDateTime;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
@@ -106,6 +105,6 @@ class InboundPlanControllerTest {
private static ManualInboundPlanRequest manualRequest() {
return new ManualInboundPlanRequest("193000000000200003", 9223372036854775807L, 0,
LocalDate.of(2026, 9, 8), 2, "STANDARD", false, true, true, "加急入仓");
2, "STANDARD", false, true, true, "加急入仓");
}
}
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.inboundplan.pda.controller;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingContainerRequests;
import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingContainerResponses;
import com.baiyue.lastleg.inboundplan.pda.gateway.DomesticLoadingEvidenceGateway;
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
import com.baiyue.lastleg.inboundplan.pda.service.ReceivingContainerService;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
@@ -90,6 +91,21 @@ class ReceivingContainerControllerTest {
verify(service).downloadCurrentPhoto("42", "9999");
}
@Test
void downloadDomesticLoadingPhotoDelegatesWithoutExposingTheSourceFileId() {
when(service.downloadDomesticLoadingPhoto("42", "PHOTO_STEP_1")).thenReturn(
new DomesticLoadingEvidenceGateway.DownloadedFile(
"domestic".getBytes(), "empty-container.jpg", MediaType.IMAGE_JPEG_VALUE));
ResponseEntity<byte[]> response = controller.downloadDomesticLoadingPhoto(picker(), "PHOTO_STEP_1");
assertThat(response.getStatusCodeValue()).isEqualTo(200);
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.IMAGE_JPEG);
assertThat(response.getHeaders().getCacheControl()).isEqualTo("no-store");
assertThat(response.getBody()).containsExactly("domestic".getBytes());
verify(service).downloadDomesticLoadingPhoto("42", "PHOTO_STEP_1");
}
@Test
void pickerAuthorityWithoutWarehouseBindingIsForbidden() {
AuthenticatedUser unbound = new AuthenticatedUser(42L, 0L, 0L, 8L, "receiver", "PLATFORM", "PDA",
@@ -18,11 +18,13 @@ import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingContainerOperation;
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingContainerSession;
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingFbaPalletCount;
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
import com.baiyue.lastleg.inboundplan.pda.gateway.DomesticLoadingEvidenceGateway;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingContainerOperationMapper;
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingContainerSessionMapper;
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingFbaPalletCountMapper;
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingSkuReceiptMapper;
import com.baiyue.lastleg.inboundplan.pda.mapper.ContainerLoadingEvidenceMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipCartonMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
@@ -71,6 +73,8 @@ class ReceivingContainerServiceBehaviorTest {
@Mock DropshipOrderProfileMapper dropshipProfiles;
@Mock WarehouseResourceGateway warehouse;
@Mock UnloadPhotoFileGateway unloadPhotos;
@Mock DomesticLoadingEvidenceGateway domesticEvidenceFiles;
@Mock ContainerLoadingEvidenceMapper domesticEvidence;
@Mock InboundPdaWorkflowService workflow;
private ReceivingContainerService service;
@@ -93,11 +97,14 @@ class ReceivingContainerServiceBehaviorTest {
ReflectionTestUtils.setField(service, "dropshipProfiles", dropshipProfiles);
ReflectionTestUtils.setField(service, "warehouse", warehouse);
ReflectionTestUtils.setField(service, "unloadPhotos", unloadPhotos);
ReflectionTestUtils.setField(service, "domesticEvidenceFiles", domesticEvidenceFiles);
ReflectionTestUtils.setField(service, "domesticEvidence", domesticEvidence);
ReflectionTestUtils.setField(service, "workflow", workflow);
ReflectionTestUtils.setField(service, "sourceObjectMapper", new ObjectMapper().findAndRegisterModules());
service.initializeObjectMapper();
org.mockito.Mockito.lenient().when(shippingGates.receivingAvailable(any(), any())).thenReturn(1);
org.mockito.Mockito.lenient().when(pallets.selectCount(any())).thenReturn(1L);
org.mockito.Mockito.lenient().when(domesticEvidence.listByContainer(any())).thenReturn(List.of());
}
@Test
@@ -904,6 +911,25 @@ class ReceivingContainerServiceBehaviorTest {
verifyNoInteractions(warehouse);
}
@Test
void domesticLoadingPhotoDownloadIsScopedToTheCurrentPhysicalContainer() {
ReceivingContainerSession current = session("CONT-A", "SIGNED");
current.setSourceContainerBookingId("91");
when(sessions.findActiveByOperator("OPERATOR:7")).thenReturn(current);
var evidence = new com.baiyue.lastleg.inboundplan.pda.entity.ContainerLoadingEvidence();
evidence.setSourceContainerBookingId("91"); evidence.setEvidenceType("SEAL");
evidence.setSourceFileId(501L); evidence.setFileName("seal.jpg");
when(domesticEvidence.find("91", "SEAL")).thenReturn(evidence);
var downloaded = new DomesticLoadingEvidenceGateway.DownloadedFile(
new byte[]{1, 2}, "seal.jpg", "image/jpeg");
when(domesticEvidenceFiles.download(91L, 501L)).thenReturn(downloaded);
assertThat(service.downloadDomesticLoadingPhoto("7", "SEAL")).isSameAs(downloaded);
verify(domesticEvidenceFiles).download(91L, 501L);
assertThatThrownBy(() -> service.downloadDomesticLoadingPhoto("7", "UNKNOWN"))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("400");
}
private void stubCurrent(String destinationType, String containerNo) {
InboundPlan plan = plan(containerNo);
InboundPlanLine line = line(destinationType);
@@ -409,7 +409,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
}
private ManualInboundPlanRequest manualRequest() {
return new ManualInboundPlanRequest("100", 11L, 0, LocalDate.of(2026, 9, 10),
return new ManualInboundPlanRequest("100", 11L, 0,
1, "WOOD", false, false, false, "自动生成");
}
@@ -93,6 +93,10 @@ class InboundPlanServiceTest {
org.mockito.Mockito.lenient().when(orderMapper.lockContainerWarehouseFacts(any(), any())).thenReturn(List.of(
new InboundPlanOrderMapper.ContainerWarehouseRef(
"100", 2L, "BOOKING-1", "193000000000200003")));
org.mockito.Mockito.lenient().when(orderMapper.findSourceContainerBookingId(any(), anyLong()))
.thenReturn("BOOKING-1");
org.mockito.Mockito.lenient().when(orderMapper.containerEstimatedWarehouseTimes(any()))
.thenReturn(List.of(LocalDateTime.of(2026, 9, 15, 8, 0)));
}
@Test
@@ -108,6 +112,7 @@ class InboundPlanServiceTest {
ArgumentCaptor<InboundPlan> plan = ArgumentCaptor.forClass(InboundPlan.class);
verify(planMapper).insert(plan.capture());
assertEquals(2L, plan.getValue().getSourceOrderId());
assertEquals(LocalDate.of(2026, 9, 15), plan.getValue().getPlannedArrivalDate());
assertEquals(1, plan.getValue().getLineCount());
assertEquals("PENDING_ASSIGNMENT", plan.getValue().getStatus());
assertEquals(1, plan.getValue().getPalletCount());
@@ -149,7 +154,7 @@ class InboundPlanServiceTest {
when(orderMapper.markPlannedManual(eq("100"), eq(2L), any(), eq(2), eq(true), eq(0), any()))
.thenReturn(1);
ManualInboundPlanRequest request = new ManualInboundPlanRequest("100", 2L, 0,
LocalDate.of(2026, 9, 15), 2, "STANDARD", true, true, true, "优先收货");
2, "STANDARD", true, true, true, "优先收货");
var result = service.convertManual(request, "manual-2", "900");
@@ -167,6 +172,27 @@ class InboundPlanServiceTest {
verify(orderMapper).lockContainerWarehouseFacts("100", 2L);
}
@Test
void manualPlanRejectsMissingOrInconsistentContainerArrivalTime() {
ManualInboundPlanRequest request = new ManualInboundPlanRequest("100", 2L, 0,
2, "STANDARD", true, true, true, "优先收货");
when(orderMapper.containerEstimatedWarehouseTimes("BOOKING-1")).thenReturn(List.of());
ResponseStatusException missing = assertThrows(ResponseStatusException.class,
() -> service.convertManual(request, "manual-missing-arrival", "900"));
assertEquals(409, missing.getStatusCode().value());
assertEquals("来源柜预计到仓时间缺失,请先补齐柜级时间", missing.getReason());
when(orderMapper.containerEstimatedWarehouseTimes("BOOKING-1")).thenReturn(List.of(
LocalDateTime.of(2026, 9, 15, 8, 0), LocalDateTime.of(2026, 9, 16, 8, 0)));
ResponseStatusException inconsistent = assertThrows(ResponseStatusException.class,
() -> service.convertManual(request, "manual-inconsistent-arrival", "900"));
assertEquals(409, inconsistent.getStatusCode().value());
assertEquals("来源柜预计到仓时间不一致,请先修正柜级时间", inconsistent.getReason());
verify(orderMapper, never()).lockOrder(any(), any());
verify(planMapper, never()).insert(any(InboundPlan.class));
}
@Test
void manualPlanMayExceedButCannotUndercutPreviewPalletCount() {
SourceOrderSnapshot source = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
@@ -175,7 +201,7 @@ class InboundPlanServiceTest {
null, null, "Amazon ACY2", 0, 0, false, 0);
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(source));
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
new ManualInboundPlanRequest("100", 2L, 0, LocalDate.of(2026, 9, 15),
new ManualInboundPlanRequest("100", 2L, 0,
2, "WOOD", false, false, false, null), "manual-below-preview", "900"));
assertEquals(400, error.getStatusCode().value());
@@ -192,7 +218,7 @@ class InboundPlanServiceTest {
new DropshipPalletSnapshot("DP-260914-0001", 10L, List.of())));
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
new ManualInboundPlanRequest("100", 2L, 0, LocalDate.of(2026, 9, 15),
new ManualInboundPlanRequest("100", 2L, 0,
2, "WOOD", false, false, false, null), "dropship-pallet-mismatch", "900"));
assertEquals(400, error.getStatusCode().value());
@@ -212,7 +238,7 @@ class InboundPlanServiceTest {
.thenReturn(1);
var result = service.convertManual(new ManualInboundPlanRequest("100", 2L, 0,
LocalDate.of(2026, 9, 15), 1, "WOOD", false, false, false, null),
1, "WOOD", false, false, false, null),
"dropship-exact-pallet-count", "900");
assertEquals(1, result.palletCount());
@@ -266,7 +292,7 @@ class InboundPlanServiceTest {
.thenReturn(1);
var result = service.convertManual(new ManualInboundPlanRequest("100", 2L, 0,
LocalDate.of(2026, 9, 15), 3, "STANDARD", true, false, false, null),
3, "STANDARD", true, false, false, null),
"manual-id-only", "900");
assertEquals("US-LAX-01", result.overseasWarehouseCode());
@@ -282,7 +308,7 @@ class InboundPlanServiceTest {
new InboundPlanOrderMapper.ContainerWarehouseRef("200", 3L, "BOOKING-1", "193000000000200004")));
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
new ManualInboundPlanRequest("100", 2L, 0, LocalDate.of(2026, 9, 15),
new ManualInboundPlanRequest("100", 2L, 0,
1, "STANDARD", false, false, false, null), "mixed-warehouse", "900"));
assertEquals(409, error.getStatusCode().value());
@@ -300,7 +326,7 @@ class InboundPlanServiceTest {
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(held));
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
new ManualInboundPlanRequest("100", 2L, 5, LocalDate.of(2026, 9, 15),
new ManualInboundPlanRequest("100", 2L, 5,
1, "STANDARD", false, false, false, null),
"manual-held", "900"));