This commit is contained in:
2026-09-15 15:15:46 +08:00
parent 419950482f
commit cadc5a175e
28 changed files with 879 additions and 89 deletions
@@ -9,6 +9,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import jakarta.validation.Validation;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -24,6 +26,18 @@ class RelabelContractTest {
"newSkuCode", "labelRequirement", "relabelFileId")));
}
@Test
void createAllowsChargeAmountToBeOmitted() {
RelabelRequests.Create request = new RelabelRequests.Create("CUSTOMER-1", "WH-1",
java.time.LocalDateTime.of(2026, 8, 20, 10, 0), "Li", "13800000000", null, null, "key-free",
java.util.List.of(new RelabelRequests.Item("SKU-A", null, null, null, "Product", 1, 1,
"SHIP-1", null, "SKU-B", "Replace", null, null, null)));
try (var validatorFactory = Validation.buildDefaultValidatorFactory()) {
assertTrue(validatorFactory.getValidator().validate(request).stream()
.noneMatch(violation -> "chargeAmount".equals(violation.getPropertyPath().toString())));
}
}
@Test
void schemasDefineRelabelApplicationItemsAndHistoryContracts() throws Exception {
String baseline = Files.readString(Path.of("src/main/resources/db/lastleg.sql"), StandardCharsets.UTF_8).toLowerCase();
@@ -89,6 +103,8 @@ class RelabelContractTest {
String relabelCreate = openApi.substring(openApi.indexOf(" RelabelCreate:"),
openApi.indexOf(" RelabelItem:"));
assertFalse(relabelCreate.contains("customerId"));
assertFalse(relabelCreate.substring(relabelCreate.indexOf("required:"), relabelCreate.indexOf("properties:"))
.contains("chargeAmount"));
}
@Test
@@ -96,6 +96,35 @@ class PdaRelabelServiceTest {
verify(applicationService).changeStatus(eq("100"), eq("FBR-1"), eq("7"), any(RelabelRequests.ChangeStatus.class));
}
@Test
void claimRejectsAnotherProcessingTaskOwnedBySameOperator() {
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(null);
PdaRelabelTask active = task("PROCESSING", 0);
active.setApplicationId(99L);
active.setRelabelNo("FBR-ACTIVE");
when(tasks.lockUnfinishedByOperator("7")).thenReturn(active);
ResponseStatusException error = assertThrows(ResponseStatusException.class,
() -> service.claim("7", "picker", 900L, "FBR-1", new PdaRelabelRequests.Claim("claim-other")));
assertTrue(error.getReason().contains("FBR-ACTIVE"));
verify(tasks, never()).insert(any(PdaRelabelTask.class));
}
@Test
void workPhotoDownloadValidatesTaskOwnershipAndFileScope() {
application.setStatus("PROCESSING");
when(applications.findAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.find("100", 1L)).thenReturn(task("PROCESSING", 0));
service.downloadWorkPhoto("7", 900L, "FBR-1", 99L);
verify(photoFiles).validate(eq("100"), anyLong(), eq(99L));
verify(photoFiles).download(eq("100"), anyLong(), eq(99L));
assertThrows(ResponseStatusException.class, () -> service.downloadWorkPhoto("8", 900L, "FBR-1", 100L));
assertThrows(ResponseStatusException.class, () -> service.downloadWorkPhoto("7", 901L, "FBR-1", 100L));
verify(photoFiles, never()).download(anyString(), anyLong(), eq(100L));
}
@Test
void crossWarehouseClaimIsHiddenAndCannotCreateTask() {
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
@@ -105,6 +134,57 @@ class PdaRelabelServiceTest {
verify(tasks, never()).insert(any(PdaRelabelTask.class));
}
@Test
void skuScanRequiresEverySkuAndNeverWritesSkuIntoCartonLabel() {
application.setStatus("PROCESSING");
item.setOperationType("SKU_LABEL");
item.setCartonMark("BOX-1");
item.setSkuRelabelItemsJson("[{\"skuCode\":\"OLD-A\",\"newSkuCode\":\"NEW-A\",\"quantity\":3}]");
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(task("PROCESSING", 0));
when(items.selectOne(any())).thenReturn(item);
assertThrows(ResponseStatusException.class, () -> service.scan("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.Scan(10L, "BOX-1", null, 0, "missing-sku")));
assertThrows(ResponseStatusException.class, () -> service.scan("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.Scan(10L, "BOX-1", null, 0, "wrong-quantity",
List.of(new PdaRelabelRequests.SkuVerification("OLD-A", "NEW-A", 2)))));
var result = service.scan("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.Scan(10L, "BOX-1", null, 0, "valid-sku",
List.of(new PdaRelabelRequests.SkuVerification("OLD-A", "NEW-A", 3))));
assertEquals(1, result.relabelledPackages());
verify(items, never()).updateById(any(RelabelItem.class));
assertEquals("BOX-1", persistedFacts.get(0).getOldLabel());
}
@org.junit.jupiter.params.ParameterizedTest
@org.junit.jupiter.params.provider.ValueSource(booleans = {true, false})
void mixedWorkRequiresBothStagesAndCountsBoxOnce(boolean cartonFirst) {
application.setStatus("PROCESSING");
item.setOperationType("CARTON_AND_SKU"); item.setCartonMark("BOX-1"); item.setFbaCartonMark("FBA-1");
item.setSkuRelabelItemsJson("[{\"skuCode\":\"OLD-A\",\"newSkuCode\":\"NEW-A\",\"quantity\":3}]");
var task = task("PROCESSING", 0);
var savedHistory = new ArrayList<com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory>();
when(histories.selectList(any())).thenAnswer(call -> List.copyOf(savedHistory));
when(histories.insert(any(com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory.class))).thenAnswer(call -> { savedHistory.add(call.getArgument(0)); return 1; });
when(applications.findAnyByRelabelNo("FBR-1")).thenReturn(application);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(task);
when(items.selectOne(any())).thenReturn(item);
java.util.function.IntFunction<com.baiyue.lastleg.relabel.pda.dto.PdaRelabelResponses.Detail> carton = version -> service.scanCarton("7", "picker", 900L, "FBR-1", new PdaRelabelRequests.CartonScan("BOX-1", version, "carton-" + version));
java.util.function.IntFunction<com.baiyue.lastleg.relabel.pda.dto.PdaRelabelResponses.Detail> sku = version -> service.scan("7", "picker", 900L, "FBR-1", new PdaRelabelRequests.Scan(10L, "BOX-1", null, version, "sku-" + version, List.of(new PdaRelabelRequests.SkuVerification("OLD-A", "NEW-A", 3))));
var first = (cartonFirst ? carton : sku).apply(0);
assertEquals(0, first.relabelledPackages());
assertEquals(cartonFirst ? 1 : 0, first.items().get(0).cartonProcessedPackages());
assertEquals(cartonFirst ? 0 : 1, first.items().get(0).skuProcessedPackages());
assertThrows(ResponseStatusException.class, () -> (cartonFirst ? carton : sku).apply(1));
assertThrows(ResponseStatusException.class, () -> service.complete("7", "picker", 900L, "FBR-1", new PdaRelabelRequests.Complete(List.of(501L), 1, "too-early", null)));
var second = (cartonFirst ? sku : carton).apply(1);
assertEquals(1, second.relabelledPackages());
assertEquals(1, persistedFacts.size());
assertEquals(1, second.items().get(0).cartonProcessedPackages());
assertEquals(1, second.items().get(0).skuProcessedPackages());
}
@Test
void unitScanLocksItemToApplicationValidatesNewSkuAndUpdatesVersion() {
application.setStatus("PROCESSING");
@@ -167,40 +247,51 @@ class PdaRelabelServiceTest {
verify(photoFiles).validate("100", 20L, 501L);
assertEquals(PdaRelabelService.WAIT_REVIEW, result.taskStatus());
assertEquals("PROCESSING", result.applicationStatus());
verify(applicationService, never()).changeStatus(anyString(), anyString(), anyString(), any());
assertEquals("CUSTOMER_CONFIRM_PENDING", result.applicationStatus());
verify(applicationService).changeStatus(eq("100"), eq("FBR-1"), eq("7"), any());
}
@Test
void approvalUsesExistingApplicationCompletionAndReworkDoesNot() {
application.setStatus("PROCESSING");
PdaRelabelTask approved = task("WAIT_REVIEW", 2);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(approved);
var result = service.review("8", "reviewer", 900L, "FBR-1",
new PdaRelabelRequests.Review("APPROVE", 2, "review-1", "ok"));
assertEquals("COMPLETED", result.taskStatus());
assertEquals(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, result.applicationStatus());
verify(applicationService).changeStatus(eq("100"), eq("FBR-1"), eq("8"), any(RelabelRequests.ChangeStatus.class));
assertThrows(ResponseStatusException.class, () -> service.review("8", "reviewer", 900L, "FBR-1",
new PdaRelabelRequests.Review("APPROVE", 2, "review-1", "ok")));
verify(applicationService, never()).changeStatus(anyString(), anyString(), anyString(), any());
}
@Test
void reviewCanReturnTaskForReworkWithoutCompletingApplication() {
application.setStatus("PROCESSING");
application.setStatus("CUSTOMER_CONFIRM_PENDING");
PdaRelabelTask task = task("WAIT_REVIEW", 2);
task.setRelabelledPackages(1);
task.setCompletionPhotoFileIds("[501]");
when(applications.findAnyByRelabelNo("FBR-1")).thenReturn(application);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(task);
var result = service.review("8", "reviewer", 900L, "FBR-1",
var result = service.returnForRework("8", "reviewer", "FBR-1",
new PdaRelabelRequests.Review("REWORK", 2, "review-rework", "wrong label"));
assertEquals("PROCESSING", result.taskStatus());
assertEquals("PROCESSING", result.applicationStatus());
assertEquals(0, result.relabelledPackages());
assertEquals("wrong label", result.reviewRemark());
assertTrue(result.completionPhotoFileIds().isEmpty());
verify(applicationService).resetForRework("100", "FBR-1", "8", "wrong label");
verify(applicationService, never()).changeStatus(anyString(), anyString(), anyString(), any());
}
@Test
void reworkRejectsMissingRemarkAndConfirmedApplication() {
assertThrows(ResponseStatusException.class, () -> service.returnForRework("8", "reviewer", "FBR-1",
new PdaRelabelRequests.Review("REWORK", 2, "empty-reason", " ")));
application.setStatus("READY_FOR_OUTBOUND");
when(applications.findAnyByRelabelNo("FBR-1")).thenReturn(application);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
assertThrows(ResponseStatusException.class, () -> service.returnForRework("8", "reviewer", "FBR-1",
new PdaRelabelRequests.Review("REWORK", 2, "confirmed", "wrong label")));
verify(applicationService, never()).resetForRework(anyString(), anyString(), anyString(), anyString());
}
@Test
void completionRejectsMissingPackageFactsBeforePhotoValidation() {
application.setStatus("PROCESSING");
@@ -75,6 +75,7 @@ class RelabelApplicationServiceTest {
@Mock OutboundPdaTaskMapper outboundTasks;
@Mock WarehouseTransferOrderMapper warehouseTransfers;
@Mock OverseasFeeSourceService feeSources;
@Mock com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper pdaTasks;
@Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
private RelabelApplicationService service;
@@ -82,6 +83,12 @@ class RelabelApplicationServiceTest {
@BeforeEach
void setUp() {
service = new RelabelApplicationService();
ReflectionTestUtils.setField(service, "pdaTasks", pdaTasks);
lenient().when(pdaTasks.lock(any(), any())).thenAnswer(call -> {
var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask();
task.setStatus("WAIT_REVIEW"); task.setVersion(1); return task;
});
lenient().when(pdaTasks.updateWithVersion(any(), any())).thenReturn(1);
ReflectionTestUtils.setField(service, "applications", applications);
ReflectionTestUtils.setField(service, "items", items);
ReflectionTestUtils.setField(service, "histories", histories);
@@ -152,6 +159,31 @@ class RelabelApplicationServiceTest {
assertEquals("FBR-1", result.application().relabelNo());
}
@Test
void createWithoutChargeSkipsWalletFreeze() {
RelabelRequests.Create charged = request("key-free", 10, 6);
RelabelRequests.Create request = new RelabelRequests.Create(charged.customerOrderNo(),
charged.operationWarehouseId(), charged.labelDeadline(), charged.contactName(), charged.contactPhone(),
null, charged.remark(), charged.idempotencyKey(), charged.items(), charged.deliveryMethod(),
charged.printShippingLabel());
RelabelApplication persisted = application(11L, "FBR-FREE", "10001", "key-free", null,
RelabelApplicationService.PENDING_PROCESSING);
persisted.setLastlegOrderId(21L);
when(applications.findNo(eq("10001"), any())).thenReturn(persisted);
when(items.selectList(any())).thenReturn(List.of(item(31L, 11L, "SKU-A", 10, 6)));
when(histories.selectList(any())).thenReturn(List.of());
when(orders.selectById(21L)).thenReturn(order(21L, "10001", "LMO-RELABEL-FREE",
RelabelApplicationService.PENDING_PROCESSING));
service.create("10001", "operator-1", request);
ArgumentCaptor<RelabelApplication> application = ArgumentCaptor.forClass(RelabelApplication.class);
verify(applications).insert(application.capture());
assertEquals(BigDecimal.ZERO, application.getValue().getChargeAmount());
assertEquals("NOT_REQUIRED", application.getValue().getWalletStatus());
verify(walletCommands, never()).publish(any(), any(Long.class), any(), any(), any(), any(), any(Long.class));
}
@Test
void partialRelabelFreezesOnlyNotifiedPackages() {
RelabelRequests.Create request = request("key-partial", 10, 2);
@@ -363,6 +395,23 @@ class RelabelApplicationServiceTest {
verify(applications).findNo("10002", "FBR-1");
}
@Test
void applicationWithoutChargeCanEnterProcessing() {
RelabelApplication application = application(11L, "FBR-FREE", "10001", "key-free", "hash",
RelabelApplicationService.PENDING_PROCESSING);
application.setLastlegOrderId(21L);
application.setChargeAmount(BigDecimal.ZERO);
application.setWalletStatus("NOT_REQUIRED");
when(applications.lockNo("10001", "FBR-FREE")).thenReturn(application);
stubDetail(application);
service.changeStatus("10001", "FBR-FREE", "operator-2",
new RelabelRequests.ChangeStatus("PROCESSING", "started"));
assertEquals("PROCESSING", application.getStatus());
verify(walletCommands, never()).publish(any(), any(Long.class), any(), any(), any(), any(), any(Long.class));
}
@Test
void completedReturnRelabelOrderEntersPendingAssignment() {
RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash", RelabelApplicationService.READY_FOR_OUTBOUND);
@@ -414,6 +463,10 @@ class RelabelApplicationServiceTest {
assertEquals(RelabelApplicationService.READY_FOR_OUTBOUND, application.getStatus());
assertEquals("WAIT_PRE_DISPATCH", application.getFulfillmentStatus());
var review = ArgumentCaptor.forClass(com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask.class);
verify(pdaTasks).updateWithVersion(review.capture(), eq(1));
assertEquals("COMPLETED", review.getValue().getStatus());
assertEquals("CUSTOMER_CONFIRMED", review.getValue().getReviewDecision());
assertNull(application.getOutboundOrderNo());
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
@@ -262,6 +262,12 @@ class WarehouseTransferServiceTest {
new WarehouseTransferRequests.Review("REJECT", " 地址不完整 "));
assertEquals("REJECTED", result.status());
assertEquals("CANCELLED", tailOrder.getCurrentStatus());
assertEquals("REJECTED", transfer.getReviewDecision());
assertEquals("地址不完整", transfer.getReviewRemark());
assertEquals("USER-1", transfer.getReviewedBy());
assertNotNull(transfer.getReviewedAt());
assertEquals("REJECTED", result.reviewDecision());
assertEquals("地址不完整", result.reviewRemark());
var history = ArgumentCaptor.forClass(com.baiyue.lastleg.entity.LastlegOrderStatusHistory.class);
verify(orderHistories).insert(history.capture());
assertTrue(history.getValue().getRemark().endsWith("; 地址不完整"));