贴标申请,仓储转运

This commit is contained in:
2026-09-22 08:49:21 +08:00
parent 9147190d67
commit 9a8412279c
28 changed files with 549 additions and 48 deletions
@@ -44,6 +44,12 @@ class RelabelContractTest {
String migration = Files.readString(Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8).toLowerCase();
String deployment = Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"),
StandardCharsets.UTF_8).toLowerCase();
for (String schema : new String[] { baseline, deployment }) {
assertTrue(schema.contains("key idx_ll_relabel_application_order (lastleg_order_id)"));
assertFalse(schema.contains("unique key uk_ll_relabel_application_order"),
"One received return order must support multiple relabel applications for remaining stock");
assertTrue(schema.contains("unique key uk_ll_relabel_application_idempotency"));
}
for (String schema : new String[] { baseline, migration }) {
assertTrue(schema.contains("ll_relabel_application"));
assertTrue(schema.contains("ll_relabel_item"));
@@ -466,12 +466,13 @@ class RelabelApplicationServiceTest {
}
@Test
void completedReturnRelabelOrderEntersPendingAssignment() {
void completedReturnRelabelKeepsParentInboundCompleted() {
RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash", RelabelApplicationService.READY_FOR_OUTBOUND);
application.setLastlegOrderId(21L);
application.setFulfillmentStatus("PRE_DISPATCH_CREATED");
LastlegOrder order = order(21L, "10001", "LMO-RELABEL-1", "PROCESSING");
order.setOrderType("RETURN_RELABEL");
order.setRelabelOrderNo("FBR-1");
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
stubDetail(application);
when(orders.selectById(21L)).thenReturn(order);
@@ -481,10 +482,32 @@ class RelabelApplicationServiceTest {
assertEquals("COMPLETED", application.getStatus());
assertEquals("OUTBOUNDED", application.getFulfillmentStatus());
assertEquals("PENDING_ASSIGNMENT", order.getCurrentStatus());
assertEquals("INBOUND_COMPLETED", order.getCurrentStatus());
assertEquals("COMPLETED", order.getRelabelStatus());
}
@Test
void completingEarlierReturnRelabelDoesNotOverwriteLaterApplicationStatus() {
RelabelApplication application = application(11L, "FBR-FIRST", "10001", "key-1", "hash", RelabelApplicationService.READY_FOR_OUTBOUND);
application.setLastlegOrderId(21L);
application.setFulfillmentStatus("PRE_DISPATCH_CREATED");
LastlegOrder order = order(21L, "10001", "LMO-RELABEL-1", "INBOUND_COMPLETED");
order.setOrderType("RETURN_RELABEL");
order.setRelabelOrderNo("FBR-SECOND");
order.setRelabelStatus("PROCESSING");
when(applications.lockNo("10001", "FBR-FIRST")).thenReturn(application);
stubDetail(application);
when(orders.selectById(21L)).thenReturn(order);
service.changeStatus("10001", "FBR-FIRST", "operator-2",
new RelabelRequests.ChangeStatus("COMPLETED", "finished"));
assertEquals("COMPLETED", application.getStatus());
assertEquals("INBOUND_COMPLETED", order.getCurrentStatus());
assertEquals("PROCESSING", order.getRelabelStatus());
assertEquals("FBR-SECOND", order.getRelabelOrderNo());
}
@Test
void completingPartialRelabelReleasesOnlyNotifiedPackages() {
RelabelApplication application = application(11L, "FBR-PARTIAL", "10001", "key-partial", "hash", RelabelApplicationService.READY_FOR_OUTBOUND);
@@ -508,8 +531,10 @@ class RelabelApplicationServiceTest {
@Test
void truckCustomerConfirmationIsIdempotentAndWaitsForPreDispatch() {
RelabelApplication application = confirmationApplication("TRUCK", false);
LastlegOrder linked = order(21L, "10001", "LMO-RELABEL-1", "WAIT_RELABEL");
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
stubDetail(application);
when(orders.selectById(21L)).thenReturn(linked);
service.confirmCompletion("10001", "FBR-1", "customer-1",
new RelabelRequests.CustomerConfirm("confirm-1", "photos accepted"));
@@ -520,6 +545,7 @@ class RelabelApplicationServiceTest {
verify(pdaTasks).updateWithVersion(review.capture(), eq(1));
assertEquals("COMPLETED", review.getValue().getStatus());
assertEquals("CONFIRMED", review.getValue().getReviewDecision());
assertEquals("WAIT_OUTBOUND", linked.getCurrentStatus());
assertNull(application.getOutboundOrderNo());
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
@@ -555,6 +581,7 @@ class RelabelApplicationServiceTest {
@Test
void transferRelabelConfirmationReleasesTransferForDispatch() {
RelabelApplication application = confirmationApplication("TRUCK", false);
application.setLastlegOrderId(88L);
WarehouseTransferOrder transfer = new WarehouseTransferOrder();
transfer.setId(77L); transfer.setCustomerId("10001"); transfer.setRelabelNo("FBR-1");
transfer.setStatus("WAIT_RELABEL"); transfer.setLastlegOrderId(88L); transfer.setFbaWarehouseCode("ONT8");
@@ -38,6 +38,7 @@ class RelabelReturnCartonCandidateTest {
cartonItem.setProductNameZh("退货商品"); cartonItem.setQuantity(12L);
when(cartons.findAvailable("10001", "WH-1")).thenReturn(List.of(carton));
when(cartonItems.listCarton(9001L)).thenReturn(List.of(cartonItem));
when(cartons.relabelledQuantity(9001L, "SKU-RETURN")).thenReturn(1L);
List<RelabelResponses.InventoryCandidate> result = service.candidates("10001", "WH-1", "RC-WH");
@@ -45,8 +46,8 @@ class RelabelReturnCartonCandidateTest {
assertEquals("RETURN_CARTON", result.get(0).sourceType());
assertEquals(9001L, result.get(0).returnCartonId());
assertEquals("RC-WH-0001", result.get(0).cartonMark());
assertEquals(12, result.get(0).availablePackages());
assertEquals(11, result.get(0).availablePackages());
assertEquals("SKU-RETURN", result.get(0).skuItems().get(0).skuCode());
assertEquals(12, result.get(0).skuItems().get(0).quantity());
assertEquals(11, result.get(0).skuItems().get(0).quantity());
}
}
@@ -93,6 +93,22 @@ class ReturnReceiptServiceTest {
identity = new PdaWarehouseIdentity("operator-1", "仓管员", 10L);
}
@Test
void detailKeepsEveryRelabelApplicationAndItsOwnStatus() {
ReturnOrder order = order("RT-MULTI", "10", "COMPLETED", 90, 90);
order.setLastlegOrderId(99L);
when(orders.findNo("customer-1", "RT-MULTI")).thenReturn(order);
var completed = new com.baiyue.lastleg.dto.RelabelSubOrder(99L, "RL-FIRST", "COMPLETED", 10, null);
var processing = new com.baiyue.lastleg.dto.RelabelSubOrder(99L, "RL-SECOND", "PROCESSING", 5, null);
when(lastlegOrders.findRelabelSubOrders(List.of(99L))).thenReturn(List.of(completed, processing));
var result = service.detail("customer-1", "RT-MULTI").order();
assertEquals("COMPLETED", result.status());
assertEquals(List.of(completed, processing), result.relabelApplications());
assertEquals(90L, result.receivedQuantity());
}
@Test
void merchantPhotoDownloadUsesAuthenticatedCustomerAndResolvedReturnOrder() {
ReflectionTestUtils.setField(service, "photoFiles", photoFiles);
@@ -128,12 +144,26 @@ class ReturnReceiptServiceTest {
}
private void verifyMultiSkuRelabel(long firstQuantity, long secondQuantity) {
verifyMultiSkuRelabel(firstQuantity, secondQuantity, false);
}
@Test
void previouslyRelabelledReturnCanLabelItsRemainingAvailableGoods() {
verifyMultiSkuRelabel(10, 5, true);
}
private void verifyMultiSkuRelabel(long firstQuantity, long secondQuantity, boolean previousRelabel) {
ReturnOrder order = order("RT-MULTI", "10", "COMPLETED", 90, 90);
if (previousRelabel) {
order.setRelabelApplicationId(88L);
order.setRelabelNo("RL-PREVIOUS");
order.setRelabelStatus("COMPLETED");
}
order.setLastlegOrderId(99L);
LastlegOrder tail = new LastlegOrder(); tail.setId(99L);
ReturnInventoryCarton first = returnCarton(101L, "RC-A", "P-1", 50);
ReturnInventoryCarton second = returnCarton(102L, "RC-B", "P-2", 40);
when(orders.findNo("customer-1", "RT-MULTI")).thenReturn(order);
when(orders.lockCustomerNo("customer-1", "RT-MULTI")).thenReturn(order);
when(orderItems.lockOrder(1L)).thenReturn(List.of(item(11L, "SKU-A", 50, 50), item(12L, "SKU-B", 40, 40)));
when(fbaWarehouseValidator.required("200", "XLX6", null)).thenReturn(
new FbaWarehouseValidator.VerifiedFbaWarehouse(200L, "XLX6", "FBA XLX6", "US", "CA"));
@@ -152,6 +182,7 @@ class ReturnReceiptServiceTest {
"SKU_TOTAL", "200", "XLX6", "multi-key", "multi SKU acceptance"));
assertEquals("RL-MULTI", result.relabelNo());
assertEquals("RL-MULTI", order.getRelabelNo());
var application = ArgumentCaptor.forClass(RelabelApplication.class);
verify(relabelApplications).insert(application.capture());
assertEquals(firstQuantity + secondQuantity, application.getValue().getNotifiedPackageCount().longValue());
@@ -174,6 +205,45 @@ class ReturnReceiptServiceTest {
return carton;
}
@Test
void secondRelabelCannotAllocateAlreadyLabelledQuantity() {
ReturnOrder order = order("RT-MULTI", "10", "COMPLETED", 100, 100);
order.setLastlegOrderId(99L); order.setRelabelNo("RL-PREVIOUS");
LastlegOrder tail = new LastlegOrder(); tail.setId(99L);
ReturnInventoryCarton carton = returnCarton(101L, "RC-A", "P-1", 100);
when(orders.lockCustomerNo("customer-1", "RT-MULTI")).thenReturn(order);
when(orderItems.lockOrder(1L)).thenReturn(List.of(item(11L, "SKU-A", 100, 100)));
when(fbaWarehouseValidator.required("200", "XLX6", null)).thenReturn(
new FbaWarehouseValidator.VerifiedFbaWarehouse(200L, "XLX6", "FBA XLX6", "US", "CA"));
when(numbers.next(eq("RELABEL"), eq("10"), anyLong())).thenReturn("RL-SECOND");
when(lastlegOrders.selectById(99L)).thenReturn(tail);
when(cartons.findAvailable("customer-1", "10")).thenReturn(List.of(carton));
when(cartonItems.listCarton(101L)).thenReturn(List.of(returnCartonItem("SKU-A", 100)));
when(cartons.relabelledQuantity(101L, "SKU-A")).thenReturn(100L);
var error = assertThrows(ResponseStatusException.class, () -> service.createRelabel(
"customer-1", "merchant-1", "RT-MULTI", new ReturnOrderRequests.RelabelCreate(
List.of(new ReturnOrderRequests.RelabelItem("SKU-A", 1)),
"SKU_TOTAL", "200", "XLX6", "second-key", "second attempt")));
assertEquals(409, error.getStatusCode().value());
assertTrue(error.getReason().contains("可用实收数量不足"));
verify(relabelItems, never()).insert(any(RelabelItem.class));
verify(cartons, never()).updateById(any(ReturnInventoryCarton.class));
}
@Test
void relabelReplayRejectsKeyFromAnotherReturnOrder() {
ReturnOrder order = order("RT-MULTI", "10", "COMPLETED", 100, 100);
when(orders.lockCustomerNo("customer-1", "RT-MULTI")).thenReturn(order);
RelabelApplication old = new RelabelApplication(); old.setCustomerOrderNo("RT-OTHER");
when(relabelApplications.findKey("customer-1", "used-key")).thenReturn(old);
var error = assertThrows(ResponseStatusException.class, () -> service.createRelabel(
"customer-1", "merchant-1", "RT-MULTI", new ReturnOrderRequests.RelabelCreate(
List.of(new ReturnOrderRequests.RelabelItem("SKU-A", 1)),
"SKU_TOTAL", "200", "XLX6", "used-key", null)));
assertEquals(409, error.getStatusCode().value());
verifyNoInteractions(cartons, cartonItems, relabelItems, fbaWarehouseValidator);
}
private static ReturnInventoryCartonItem returnCartonItem(String sku, long quantity) {
ReturnInventoryCartonItem line = new ReturnInventoryCartonItem();
line.setSkuCode(sku); line.setQuantity(quantity); return line;
@@ -95,7 +95,7 @@ class LastlegOrderServiceTest {
LastlegOrderSummary summary = ReflectionTestUtils.invokeMethod(
service, "summary", order, List.of(), null, "UNSCHEDULED",
new LastlegOrderMapper.OrderWeightRow(11L, new java.math.BigDecimal("30"), new java.math.BigDecimal("22")));
new LastlegOrderMapper.OrderWeightRow(11L, new java.math.BigDecimal("30"), new java.math.BigDecimal("22")), List.of());
assertNotNull(summary);
assertEquals("BOOKING-1", summary.sourceContainerBookingId());