This commit is contained in:
2026-09-10 17:11:33 +08:00
parent 1ee32b7a6f
commit 2e6a18c58e
54 changed files with 2094 additions and 167 deletions
@@ -50,6 +50,13 @@ class RelabelContractTest {
"source_warehouse_code", "source_warehouse_name", "idx_ll_relabel_item_source_position" }) {
assertTrue(deployment.contains(field), () -> "deployment SQL must include " + field);
}
for (String field : new String[] { "delivery_method", "print_shipping_label", "shipping_label_file_id",
"shipping_label_file_name", "shipping_label_file_url", "fulfillment_status", "outbound_order_no",
"customer_confirmation_key", "customer_confirmed_at" }) {
assertTrue(baseline.contains(field), () -> "baseline SQL must include " + field);
assertTrue(migration.contains(field), () -> "migration SQL must include " + field);
assertTrue(deployment.contains(field), () -> "deployment SQL must include " + field);
}
}
@Test
@@ -63,6 +70,19 @@ class RelabelContractTest {
assertTrue(openApi.contains("platform:overseas-warehouse:relabel-order:list"));
assertTrue(openApi.contains("platform:overseas-warehouse:relabel-order:create"));
assertTrue(openApi.contains("platform:overseas-warehouse:relabel-order:status"));
for (String path : new String[] {
"/api/merchant/v1/last-leg/relabel-applications/inventory-candidates:",
"/api/merchant/v1/last-leg/relabel-applications:",
"/api/merchant/v1/last-leg/relabel-applications/{relabelNo}:",
"/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/shipping-label:",
"/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/shipping-label/{fileId}/download:",
"/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/completion-photos/{fileId}/download:",
"/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/confirm-completion:" }) {
assertTrue(openApi.contains(path), () -> "merchant relabel path missing: " + path);
}
assertTrue(openApi.contains("RelabelMerchantPageResponse"));
assertTrue(openApi.contains("RelabelCustomerConfirm"));
assertTrue(openApi.contains("merchant:warehouse:relabel"));
String relabelPaths = openApi.substring(openApi.indexOf("/api/platform/v1/last-leg/relabel-applications:"),
openApi.indexOf("components:"));
assertTrue(relabelPaths.contains("name: customerId, in: query, required: true"));
@@ -182,7 +182,7 @@ class PdaRelabelServiceTest {
new PdaRelabelRequests.Review("APPROVE", 2, "review-1", "ok"));
assertEquals("COMPLETED", result.taskStatus());
assertEquals("COMPLETED", result.applicationStatus());
assertEquals(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, result.applicationStatus());
verify(applicationService).changeStatus(eq("100"), eq("FBR-1"), eq("8"), any(RelabelRequests.ChangeStatus.class));
}
@@ -22,6 +22,12 @@ import com.baiyue.lastleg.inventory.entity.InventoryBalance;
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
import com.baiyue.lastleg.delivery.entity.OutboundPdaTask;
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundPdaTaskMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -60,6 +66,9 @@ class RelabelApplicationServiceTest {
@Mock InventoryCommandService inventoryCommands;
@Mock InventoryBalanceMapper inventoryBalances;
@Mock InventoryPositionMapper inventoryPositions;
@Mock OutboundOrderMapper outboundOrders;
@Mock OutboundPalletItemMapper outboundItems;
@Mock OutboundPdaTaskMapper outboundTasks;
@Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
private RelabelApplicationService service;
@@ -77,6 +86,9 @@ class RelabelApplicationServiceTest {
ReflectionTestUtils.setField(service, "inventoryCommands", inventoryCommands);
ReflectionTestUtils.setField(service, "inventoryBalances", inventoryBalances);
ReflectionTestUtils.setField(service, "inventoryPositions", inventoryPositions);
ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders);
ReflectionTestUtils.setField(service, "outboundItems", outboundItems);
ReflectionTestUtils.setField(service, "outboundTasks", outboundTasks);
ReflectionTestUtils.setField(service, "objectMapper", mapper());
lenient().when(walletCommands.publish(any(), any(Long.class), any(), any(), any(), any(), any(Long.class))).thenReturn("wallet-event-1");
lenient().when(inventoryPositions.findTenantPosition(any(), any())).thenAnswer(invocation -> {
@@ -344,7 +356,7 @@ class RelabelApplicationServiceTest {
@Test
void completedReturnRelabelOrderEntersPendingAssignment() {
RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash", "PROCESSING");
RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash", RelabelApplicationService.READY_FOR_OUTBOUND);
application.setLastlegOrderId(21L);
LastlegOrder order = order(21L, "10001", "LMO-RELABEL-1", "PROCESSING");
order.setOrderType("RETURN_RELABEL");
@@ -362,7 +374,7 @@ class RelabelApplicationServiceTest {
@Test
void completingPartialRelabelReleasesOnlyNotifiedPackages() {
RelabelApplication application = application(11L, "FBR-PARTIAL", "10001", "key-partial", "hash", "PROCESSING");
RelabelApplication application = application(11L, "FBR-PARTIAL", "10001", "key-partial", "hash", RelabelApplicationService.READY_FOR_OUTBOUND);
application.setLastlegOrderId(21L);
LastlegOrder order = order(21L, "10001", "LMO-RELABEL-PARTIAL", "PROCESSING");
when(applications.lockNo("10001", "FBR-PARTIAL")).thenReturn(application);
@@ -380,6 +392,80 @@ class RelabelApplicationServiceTest {
assertEquals(6L, move.getValue().packages());
}
@Test
void truckCustomerConfirmationIsIdempotentAndWaitsForPreDispatch() {
RelabelApplication application = confirmationApplication("TRUCK", false);
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
stubDetail(application);
service.confirmCompletion("10001", "FBR-1", "customer-1",
new RelabelRequests.CustomerConfirm("confirm-1", "photos accepted"));
assertEquals(RelabelApplicationService.READY_FOR_OUTBOUND, application.getStatus());
assertEquals("WAIT_PRE_DISPATCH", application.getFulfillmentStatus());
assertNull(application.getOutboundOrderNo());
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
service.confirmCompletion("10001", "FBR-1", "customer-1",
new RelabelRequests.CustomerConfirm("confirm-1", "photos accepted"));
verify(applications, times(1)).updateById(application);
}
@Test
void expressCustomerConfirmationRequiresShippingLabelWhenWarehousePrintingIsSelected() {
RelabelApplication application = confirmationApplication("EXPRESS", true);
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () ->
service.confirmCompletion("10001", "FBR-1", "customer-1",
new RelabelRequests.CustomerConfirm("confirm-express", null)));
assertEquals(409, exception.getStatusCode().value());
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
verify(applications, never()).updateById(application);
}
@Test
void pickupCustomerConfirmationCreatesOneExactCartonOutbound() {
RelabelApplication application = confirmationApplication("PICKUP", false);
RelabelItem source = item(31L, application.getId(), "SKU-A", 1, 1);
source.setSourceInventoryPositionId(201L);
source.setSourceInboundCode("CODE-1");
source.setCartonMark("BOX-1");
source.setDestinationFbaWarehouseCode("ONT8");
InventoryPosition position = new InventoryPosition();
position.setId(201L); position.setCustomerId("10001"); position.setPalletId(501L); position.setPalletNo("PALLET-1");
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
when(applications.findNo("10001", "FBR-1")).thenReturn(application);
when(items.selectList(any())).thenReturn(List.of(source));
when(histories.selectList(any())).thenReturn(List.of());
when(orders.selectById(21L)).thenReturn(order(21L, "10001", "LMO-RELABEL-1", "PENDING_ASSIGNMENT"));
when(inventoryPositions.selectById(201L)).thenReturn(position);
service.confirmCompletion("10001", "FBR-1", "customer-1",
new RelabelRequests.CustomerConfirm("confirm-pickup", null));
assertEquals("OUTBOUND_CREATED", application.getFulfillmentStatus());
assertNotNull(application.getOutboundOrderNo());
ArgumentCaptor<OutboundOrder> outbound = ArgumentCaptor.forClass(OutboundOrder.class);
verify(outboundOrders, times(1)).insert(outbound.capture());
assertEquals("RELABEL_OUTBOUND", outbound.getValue().getOutboundType());
ArgumentCaptor<OutboundPalletItem> outboundItem = ArgumentCaptor.forClass(OutboundPalletItem.class);
verify(outboundItems, times(1)).insert(outboundItem.capture());
assertEquals("BOX-1", outboundItem.getValue().getCartonMarks());
assertEquals("PALLET-1", outboundItem.getValue().getPalletNo());
verify(outboundTasks, times(1)).insert(any(OutboundPdaTask.class));
}
private RelabelApplication confirmationApplication(String deliveryMethod, boolean printShippingLabel) {
RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash",
RelabelApplicationService.CUSTOMER_CONFIRM_PENDING);
application.setLastlegOrderId(21L);
application.setDeliveryMethod(deliveryMethod);
application.setPrintShippingLabel(printShippingLabel);
return application;
}
private void stubDetail(RelabelApplication application) {
when(applications.findNo(application.getCustomerId(), application.getRelabelNo())).thenReturn(application);
when(items.selectList(any())).thenReturn(List.of(item(31L, application.getId(), "SKU-A", 10, 6)));
@@ -0,0 +1,80 @@
package com.baiyue.lastleg.relabel.service;
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
import com.baiyue.lastleg.relabel.entity.RelabelItem;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class RelabelPalletMutationServiceTest {
@Mock private RelabelItemMapper relabelItems;
@Mock private InventoryPositionMapper positions;
@Mock private OutboundOrderMapper outboundOrders;
@Mock private OutboundPalletItemMapper outboundItems;
private RelabelPalletMutationService service;
@BeforeEach void setUp() {
service = new RelabelPalletMutationService();
ReflectionTestUtils.setField(service, "relabelItems", relabelItems);
ReflectionTestUtils.setField(service, "positions", positions);
ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders);
ReflectionTestUtils.setField(service, "outboundItems", outboundItems);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
}
@Test void partialCartonMoveSplitsOutboundSnapshotAndKeepsUnselectedCartonOnSource() {
RelabelApplication application = new RelabelApplication(); application.setId(10L);
application.setCustomerId("100"); application.setRelabelNo("RL-1"); application.setOutboundOrderNo("OUT-1");
RelabelItem selected = item(20L, "CARTON-A", 101L);
RelabelItem remaining = item(21L, "CARTON-B", 100L);
InventoryPosition target = new InventoryPosition(); target.setId(101L); target.setPalletId(11L); target.setPalletNo("TARGET");
OutboundOrder outbound = new OutboundOrder(); outbound.setId(30L); outbound.setCustomerId("100");
outbound.setOutboundNo("OUT-1"); outbound.setOutboundType("RELABEL_OUTBOUND"); outbound.setStatus("WAIT_PICK");
OutboundPalletItem source = new OutboundPalletItem(); source.setId(31L); source.setCustomerId("100");
source.setOutboundOrderId(30L); source.setWarehousePalletId(10L); source.setPalletNo("SOURCE");
source.setPackageCount(2); source.setPickedPackageCount(0); source.setStatus("WAIT_PICK");
source.setCartonMarks("CARTON-A,CARTON-B");
when(outboundOrders.lockByNo("100", "OUT-1")).thenReturn(outbound);
when(outboundItems.findByOutbound("100", 30L)).thenReturn(List.of(source));
when(relabelItems.selectList(any())).thenReturn(List.of(remaining));
service.moveSelectedItems(application, Map.of(100L, List.of(selected)), Map.of(100L, target));
assertEquals(101L, selected.getSourceInventoryPositionId());
verify(relabelItems).updateById(selected);
assertEquals("CARTON-B", source.getCartonMarks()); assertEquals(1, source.getPackageCount());
ArgumentCaptor<OutboundPalletItem> split = ArgumentCaptor.forClass(OutboundPalletItem.class);
verify(outboundItems).insert(split.capture());
assertEquals("CARTON-A", split.getValue().getCartonMarks());
assertEquals(11L, split.getValue().getWarehousePalletId());
assertEquals(1, split.getValue().getPackageCount());
assertEquals(2, outbound.getPalletCount());
}
private static RelabelItem item(long id, String mark, long positionId) {
RelabelItem value = new RelabelItem(); value.setId(id); value.setApplicationId(10L); value.setCustomerId("100");
value.setCartonMark(mark); value.setNotifiedPackageCount(1); value.setSourceInventoryPositionId(positionId);
value.setDestinationFbaWarehouseCode("ONT8"); return value;
}
}