This commit is contained in:
2026-10-15 15:42:10 +08:00
parent 095a95e64a
commit c6bd6af5ce
43 changed files with 1153 additions and 92 deletions
@@ -3,10 +3,51 @@ package com.baiyue.lastleg.discard;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.sql.DriverManager;
import java.sql.SQLException;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
import org.apache.ibatis.annotations.Update;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CargoDiscardSchemaContractTest {
@Test
void returnCartonDeductionAllowsZeroButRejectsNegativeStock() throws Exception {
String baseline;
try (var input = getClass().getResourceAsStream("/db/lastleg.sql")) {
baseline = new String(input.readAllBytes(), StandardCharsets.UTF_8);
}
var constraint = java.util.regex.Pattern.compile(
"CONSTRAINT chk_ll_return_inventory_carton_item_quantity CHECK\\(([^)]+)\\)")
.matcher(baseline);
assertTrue(constraint.find());
String deduction = ReturnInventoryCartonItemMapper.class.getMethod("deduct", long.class, long.class)
.getAnnotation(Update.class).value()[0]
.replace("#{quantity}", "?").replace("#{id}", "?");
try (var connection = DriverManager.getConnection("jdbc:h2:mem:discard_zero_quantity;MODE=MySQL")) {
try (var statement = connection.createStatement()) {
statement.execute("CREATE TABLE ll_return_inventory_carton_item (id BIGINT PRIMARY KEY, "
+ "quantity BIGINT NOT NULL, CONSTRAINT chk_ll_return_inventory_carton_item_quantity CHECK("
+ constraint.group(1) + "))");
statement.execute("INSERT INTO ll_return_inventory_carton_item VALUES (1, 3)");
try (var update = connection.prepareStatement(deduction)) {
update.setLong(1, 3); update.setLong(2, 1); update.setLong(3, 3);
assertEquals(1, update.executeUpdate());
update.setLong(1, 1); update.setLong(3, 1);
assertEquals(0, update.executeUpdate());
}
try (var result = statement.executeQuery("SELECT quantity FROM ll_return_inventory_carton_item WHERE id=1")) {
assertTrue(result.next());
assertEquals(0, result.getLong(1));
}
assertThrows(SQLException.class, () -> statement.executeUpdate(
"UPDATE ll_return_inventory_carton_item SET quantity=-1 WHERE id=1"));
}
}
}
@Test
void baselineContainsDiscardApplicationReservationAndHistoryTables() throws Exception {
String sql;
@@ -15,6 +15,22 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class MerchantCargoDiscardControllerTest {
@Test
void completionUsesAuthenticatedCustomerAndIndependentPermission() throws Exception {
controller.confirmCompletion(user(100L, "MERCHANT"), "CD001");
verify(service).confirmCompletion("100", "CD001", "1");
assertThrows(ResponseStatusException.class,
() -> controller.confirmCompletion(user(0L, "PLATFORM"), "CD001"));
var guard = MerchantCargoDiscardController.class.getMethod("confirmCompletion", AuthenticatedUser.class, String.class)
.getAnnotation(org.springframework.security.access.prepost.PreAuthorize.class);
org.junit.jupiter.api.Assertions.assertEquals(
"hasAuthority('merchant:overseas-warehouse:discard:confirm')", guard.value());
org.junit.jupiter.api.Assertions.assertFalse(java.util.Arrays.stream(CargoDiscardController.class.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(org.springframework.web.bind.annotation.PostMapping.class))
.flatMap(method -> java.util.Arrays.stream(method.getAnnotation(org.springframework.web.bind.annotation.PostMapping.class).value()))
.anyMatch(path -> path.endsWith("/complete") || path.endsWith("/confirm-completion")));
}
private final CargoDiscardService service = mock(CargoDiscardService.class);
private final MerchantCargoDiscardController controller = controller();
private final CargoDiscardRequests.Query query = new CargoDiscardRequests.Query(
@@ -43,6 +43,7 @@ class CargoDiscardOutboundServiceTest {
application = new CargoDiscardApplication(); application.setId(1L); application.setCustomerId("C1");
application.setDiscardNo("CD-TEST"); application.setOverseasWarehouseId("99");
application.setTotalPackages(5); application.setStatus("APPROVED");
application.setInboundCodes("RT-123,RK-456");
first = line(11L, 21L, 3); second = line(12L, 22L, 2);
when(items.listApplication("C1", 1L)).thenReturn(List.of(first, second));
when(cartons.listApplication("C1", 1L)).thenReturn(List.of());
@@ -63,6 +64,7 @@ class CargoDiscardOutboundServiceTest {
@Test void approvalCreatesExactlyOneDocumentAndImmutablePlannedLines() {
service.create(application, "OP"); service.create(application, "OP");
assertEquals("DISCARD_OUTBOUND", document.getOutboundType()); assertEquals("WAIT_PICK", document.getStatus());
assertNull(document.getLxCodes());
assertEquals(5, document.getPackageCount()); assertEquals(2, document.getPalletCount());
assertTrue(service.outboundNo(application).startsWith("OB-"));
assertEquals(List.of(3, 2), snapshots.stream().map(OutboundPalletItem::getPackageCount).toList());
@@ -82,6 +84,13 @@ class CargoDiscardOutboundServiceTest {
service.create(application, "OP"); application.setStatus("CANCELLED"); service.sync(application);
assertEquals("CANCELLED", document.getStatus()); assertTrue(snapshots.stream().allMatch(s -> "CANCELLED".equals(s.getStatus())));
}
@Test void syncClearsHistoricallyMislabelledInboundCodesWithoutChangingSourceSnapshots() {
first.setInboundCode("RK-456");
service.create(application, "OP"); document.setLxCodes("RT-123,RK-456");
service.sync(application);
assertNull(document.getLxCodes()); assertEquals("RK-456", snapshots.get(0).getInboundCode());
assertEquals("RT-123,RK-456", application.getInboundCodes());
}
@Test void returnSourceKeepsRealCartonMarkWithoutInventingPalletNumber() {
first.setReturnCartonId(31L); first.setSourceType("RETURN_CARTON");
var carton = new ReturnInventoryCarton(); carton.setId(31L); carton.setCustomerId("C1");
@@ -47,6 +47,36 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CargoDiscardServiceTest {
@Test
void customerConfirmationClosesPdaCompletedApplicationOnceWithoutAnotherDeduction() {
CargoDiscardApplication application = new CargoDiscardApplication();
application.setId(1001L); application.setCustomerId("100"); application.setDiscardNo("CD001");
application.setTotalPackages(1); application.setOverseasWarehouseName("测试仓"); application.setStatus(CargoDiscardService.PDA_COMPLETED);
storedApplication.set(application);
when(applications.lockNo("100", "CD001")).thenReturn(application);
service.confirmCompletion("100", "CD001", "CUSTOMER");
var confirmedAt = application.getCustomerConfirmedAt();
org.junit.jupiter.api.Assertions.assertNotNull(confirmedAt);
assertEquals(CargoDiscardService.COMPLETED, application.getStatus());
assertEquals("客户确认废弃完成", storedHistories.get(0).getRemark());
service.confirmCompletion("100", "CD001", "CUSTOMER");
assertEquals(confirmedAt, application.getCustomerConfirmedAt());
assertEquals(1, storedHistories.size());
verify(applications, times(1)).updateById(application);
org.mockito.Mockito.verifyNoInteractions(adjustments, inventory, returnCartonItems);
}
@org.junit.jupiter.params.ParameterizedTest
@org.junit.jupiter.params.provider.ValueSource(strings = {"PENDING_REVIEW", "APPROVED", "REJECTED", "CANCELLED"})
void customerCannotConfirmBeforePdaCompletion(String status) {
CargoDiscardApplication application = new CargoDiscardApplication(); application.setStatus(status);
when(applications.lockNo("100", "CD001")).thenReturn(application);
org.junit.jupiter.api.Assertions.assertThrows(org.springframework.web.server.ResponseStatusException.class,
() -> service.confirmCompletion("100", "CD001", "CUSTOMER"));
verify(applications, never()).updateById(any(CargoDiscardApplication.class));
}
@Mock CargoDiscardApplicationMapper applications;
@Mock CargoDiscardItemMapper items;
@Mock CargoDiscardCartonMapper discardCartons;
@@ -57,6 +87,7 @@ class CargoDiscardServiceTest {
@Mock LastlegOrderStatusHistoryMapper orderHistories;
@Mock ReturnInventoryCartonMapper returnCartons;
@Mock ReturnInventoryCartonItemMapper returnCartonItems;
@Mock com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient warehouseDirectory;
private CargoDiscardService service;
private final CargoDiscardOutboundService outboundService = org.mockito.Mockito.mock(CargoDiscardOutboundService.class);
@@ -68,6 +99,9 @@ class CargoDiscardServiceTest {
@BeforeEach
void setUp() {
service = new CargoDiscardService();
ReflectionTestUtils.setField(service, "warehouseDirectory", warehouseDirectory);
lenient().when(warehouseDirectory.receivingWarehouses(any())).thenReturn(com.baiyue.common.core.api.ApiResponse.success(List.of(
new com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient.ReceivingWarehouse(99L, "LAX", "洛杉矶仓", "US", "ACTIVE"))));
ReflectionTestUtils.setField(service, "outboundService", outboundService);
ReflectionTestUtils.setField(service, "stockService", org.mockito.Mockito.mock(CargoDiscardStockService.class));
ReflectionTestUtils.setField(service, "photoService", org.mockito.Mockito.mock(CargoDiscardPhotoService.class));
@@ -97,6 +131,7 @@ class CargoDiscardServiceTest {
@Test
void createReservesExactSkuCartonsWithoutDeductingInventory() {
LastlegOrder order = order();
order.setOverseasWarehouseId("99"); order.setOverseasWarehouseName(null);
DropshipSkuInventory sku = skuInventory();
when(orders.selectById(101L)).thenReturn(order);
when(inventory.countAvailable("100", 101L, "SKU-1")).thenReturn(2L);
@@ -109,6 +144,7 @@ class CargoDiscardServiceTest {
List.of(new CargoDiscardRequests.Item(101L, null, "SKU-1", 2)), "discard-create-1", "damaged"));
assertEquals(CargoDiscardService.PENDING_REVIEW, detail.summary().status());
assertEquals("洛杉矶仓", storedApplication.get().getOverseasWarehouseName());
assertEquals(2, detail.summary().totalPackages());
assertEquals(new BigDecimal("10.00"), detail.summary().totalFee());
assertEquals(2, storedLinks.size());
@@ -116,6 +152,14 @@ class CargoDiscardServiceTest {
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
}
@Test void historicalDiscardDetailResolvesMissingWarehouseNameByOperatingWarehouseId() {
CargoDiscardApplication application = pendingApplication();
application.setOverseasWarehouseId("99"); application.setOverseasWarehouseName(null);
storedApplication.set(application);
assertEquals("洛杉矶仓", service.detail("100", application.getDiscardNo()).summary().overseasWarehouseName());
verify(applications, never()).updateById(any(CargoDiscardApplication.class));
}
@Test void selectedBoxesCreatePartialApplicationWithoutReservingUnselectedStock() {
when(orders.selectById(101L)).thenReturn(order());
@@ -159,7 +203,7 @@ class CargoDiscardServiceTest {
ReturnInventoryCarton carton = new ReturnInventoryCarton();
carton.setId(901L);
carton.setCustomerId("100");
carton.setOverseasWarehouseId("WH-1");
carton.setOverseasWarehouseId("99");
carton.setSourceReturnNo("RT-20260928-001");
carton.setCartonMark("RC-LAX-001");
carton.setStatus("AVAILABLE");
@@ -176,6 +220,7 @@ class CargoDiscardServiceTest {
assertEquals(1, result.size());
assertEquals("RETURN_CARTON", result.get(0).sourceType());
assertEquals("洛杉矶仓", result.get(0).overseasWarehouseName());
assertEquals(901L, result.get(0).returnCartonId());
assertEquals("RC-LAX-001", result.get(0).cartonMark());
assertEquals(4, result.get(0).availablePackages());