暂存提交

This commit is contained in:
2026-08-19 16:24:02 +08:00
parent f5c4bf130a
commit 6acb6178da
354 changed files with 25000 additions and 198 deletions
@@ -0,0 +1,108 @@
package com.baiyue.lastleg.discard.service;
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
import com.baiyue.lastleg.discard.entity.CargoDiscardCarton;
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
import com.baiyue.lastleg.dropship.mapper.DropshipCartonLedgerMapper;
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 org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CargoDiscardInventoryAdjustmentServiceTest {
@Mock CargoDiscardInventoryMapper inventory;
@Mock DropshipCartonLedgerMapper ledgers;
private CargoDiscardInventoryAdjustmentService service;
@BeforeEach
void setUp() {
service = new CargoDiscardInventoryAdjustmentService();
ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "cartonLedgers", ledgers);
}
@Test
void discardRequiresExistingTransactionAndConservesPackages() throws Exception {
Transactional transactional = CargoDiscardInventoryAdjustmentService.class
.getMethod("discard", String.class, CargoDiscardApplication.class, List.class, Map.class,
String.class, LocalDateTime.class).getAnnotation(Transactional.class);
assertEquals(Propagation.MANDATORY, transactional.propagation());
CargoDiscardApplication application = application(2);
CargoDiscardItem item = item(2);
when(inventory.lockSkuInventory("100", 301L)).thenReturn(sku(5));
when(inventory.deductSkuInventory("100", 301L, 2)).thenReturn(1);
when(inventory.skuPackages("100", 301L)).thenReturn(3L);
when(inventory.lockCarton(eq("100"), anyLong())).thenAnswer(invocation -> carton(invocation.getArgument(1)));
when(inventory.changeCartonStatus(eq("100"), anyLong(), eq("DISCARD_PENDING"), eq("DISCARDED"))).thenReturn(1);
var result = service.discard("100", application, List.of(item),
Map.of(item.getId(), List.of(link(1L), link(2L))), "9", LocalDateTime.now());
assertEquals(5, result.beforePackages());
assertEquals(2, result.discardedPackages());
assertEquals(3, result.afterPackages());
ArgumentCaptor<DropshipCartonLedger> facts = ArgumentCaptor.forClass(DropshipCartonLedger.class);
verify(ledgers, times(2)).insert(facts.capture());
assertTrue(facts.getAllValues().stream().allMatch(value -> "DISCARD".equals(value.getOperationType())
&& value.getIdempotencyKey().startsWith("CARGO_DISCARD:401:")));
}
@Test
void discardRejectsAnyPackageConservationMismatch() {
CargoDiscardApplication application = application(2);
CargoDiscardItem item = item(2);
when(inventory.lockSkuInventory("100", 301L)).thenReturn(sku(5));
when(inventory.deductSkuInventory("100", 301L, 2)).thenReturn(1);
when(inventory.skuPackages("100", 301L)).thenReturn(4L);
when(inventory.lockCarton(eq("100"), anyLong())).thenAnswer(invocation -> carton(invocation.getArgument(1)));
when(inventory.changeCartonStatus(eq("100"), anyLong(), eq("DISCARD_PENDING"), eq("DISCARDED"))).thenReturn(1);
assertThrows(ResponseStatusException.class, () -> service.discard("100", application, List.of(item),
Map.of(item.getId(), List.of(link(1L), link(2L))), "9", LocalDateTime.now()));
}
private CargoDiscardApplication application(int packages) {
CargoDiscardApplication value = new CargoDiscardApplication();
value.setId(401L); value.setDiscardNo("FH-1"); value.setTotalPackages(packages); value.setRemark("damaged");
return value;
}
private CargoDiscardItem item(int packages) {
CargoDiscardItem value = new CargoDiscardItem();
value.setId(501L); value.setOrderId(101L); value.setSkuInventoryId(301L); value.setSkuCode("SKU-1");
value.setDiscardPackages(packages); return value;
}
private DropshipSkuInventory sku(long packages) {
DropshipSkuInventory value = new DropshipSkuInventory(); value.setId(301L); value.setPackageCount(packages); return value;
}
private CargoDiscardCarton link(long id) {
CargoDiscardCarton value = new CargoDiscardCarton(); value.setItemId(501L); value.setCartonId(id); return value;
}
private DropshipCarton carton(long id) {
DropshipCarton value = new DropshipCarton(); value.setId(id); value.setCartonMark("C-" + id); return value;
}
}
@@ -0,0 +1,180 @@
package com.baiyue.lastleg.discard.service;
import com.baiyue.lastleg.discard.dto.CargoDiscardRequests;
import com.baiyue.lastleg.discard.entity.CargoDiscardApplication;
import com.baiyue.lastleg.discard.entity.CargoDiscardCarton;
import com.baiyue.lastleg.discard.entity.CargoDiscardItem;
import com.baiyue.lastleg.discard.entity.CargoDiscardStatusHistory;
import com.baiyue.lastleg.discard.mapper.CargoDiscardApplicationMapper;
import com.baiyue.lastleg.discard.mapper.CargoDiscardCartonMapper;
import com.baiyue.lastleg.discard.mapper.CargoDiscardInventoryMapper;
import com.baiyue.lastleg.discard.mapper.CargoDiscardItemMapper;
import com.baiyue.lastleg.discard.mapper.CargoDiscardStatusHistoryMapper;
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CargoDiscardServiceTest {
@Mock CargoDiscardApplicationMapper applications;
@Mock CargoDiscardItemMapper items;
@Mock CargoDiscardCartonMapper discardCartons;
@Mock CargoDiscardStatusHistoryMapper histories;
@Mock CargoDiscardInventoryMapper inventory;
@Mock CargoDiscardInventoryAdjustmentService adjustments;
@Mock LastlegOrderMapper orders;
private CargoDiscardService service;
private final AtomicReference<CargoDiscardApplication> storedApplication = new AtomicReference<>();
private final List<CargoDiscardItem> storedItems = new ArrayList<>();
private final List<CargoDiscardCarton> storedLinks = new ArrayList<>();
private final List<CargoDiscardStatusHistory> storedHistories = new ArrayList<>();
@BeforeEach
void setUp() {
service = new CargoDiscardService();
ReflectionTestUtils.setField(service, "applications", applications);
ReflectionTestUtils.setField(service, "items", items);
ReflectionTestUtils.setField(service, "discardCartons", discardCartons);
ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "adjustments", adjustments);
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(service, "feePerPackage", new BigDecimal("5.00"));
lenient().when(applications.insert(any(CargoDiscardApplication.class))).thenAnswer(invocation -> { storedApplication.set(invocation.getArgument(0)); return 1; });
lenient().when(items.insert(any(CargoDiscardItem.class))).thenAnswer(invocation -> { storedItems.add(invocation.getArgument(0)); return 1; });
lenient().when(discardCartons.insert(any(CargoDiscardCarton.class))).thenAnswer(invocation -> { storedLinks.add(invocation.getArgument(0)); return 1; });
lenient().when(histories.insert(any(CargoDiscardStatusHistory.class))).thenAnswer(invocation -> { storedHistories.add(invocation.getArgument(0)); return 1; });
lenient().when(applications.findNo(any(), any())).thenAnswer(invocation -> storedApplication.get());
lenient().when(items.listApplication(any(), anyLong())).thenAnswer(invocation -> List.copyOf(storedItems));
lenient().when(discardCartons.listApplication(any(), anyLong())).thenAnswer(invocation -> List.copyOf(storedLinks));
lenient().when(histories.listApplication(any(), anyLong())).thenAnswer(invocation -> List.copyOf(storedHistories));
}
@Test
void createReservesExactSkuCartonsWithoutDeductingInventory() {
LastlegOrder order = order();
DropshipSkuInventory sku = skuInventory();
when(orders.selectById(101L)).thenReturn(order);
when(inventory.countAvailable("100", 101L, "SKU-1")).thenReturn(3L);
when(inventory.lockAvailable("100", 101L, "SKU-1", 2)).thenReturn(List.of(carton(1L), carton(2L)));
when(inventory.lockSkuInventoryDimension("100", 101L, 201L, "SKU-1")).thenReturn(sku);
when(inventory.countAvailableProfile("100", 101L, 201L, "SKU-1")).thenReturn(3L);
when(inventory.changeCartonStatus(eq("100"), anyLong(), eq("AVAILABLE"), eq("DISCARD_PENDING"))).thenReturn(1);
var detail = service.create("100", "7", new CargoDiscardRequests.Create(
List.of(new CargoDiscardRequests.Item(101L, "SKU-1", 2)), "discard-create-1", "damaged"));
assertEquals(CargoDiscardService.PENDING_REVIEW, detail.summary().status());
assertEquals(2, detail.summary().totalPackages());
assertEquals(new BigDecimal("10.00"), detail.summary().totalFee());
assertEquals(2, storedLinks.size());
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
}
@Test
void approvalDeductsSkuInventoryAndWritesOneLedgerPerCarton() {
CargoDiscardApplication application = pendingApplication();
storedApplication.set(application);
CargoDiscardItem item = item();
storedItems.add(item);
storedLinks.add(link(item.getId(), 1L));
storedLinks.add(link(item.getId(), 2L));
when(applications.lockNo("100", "FH-1")).thenReturn(application);
when(adjustments.discard(eq("100"), eq(application), any(), any(), eq("9"), any()))
.thenReturn(new CargoDiscardInventoryAdjustmentService.AdjustmentResult(3, 2, 1));
var detail = service.review("100", "FH-1", "9", new CargoDiscardRequests.Review("APPROVE", "ok"));
assertEquals(CargoDiscardService.APPROVED, detail.summary().status());
verify(adjustments).discard(eq("100"), eq(application), any(), any(), eq("9"), any());
assertEquals("ok; inventoryBefore=3, discarded=2, inventoryAfter=1",
storedHistories.get(storedHistories.size() - 1).getRemark());
}
@Test
void rejectionReleasesReservedCartonsWithoutDeduction() {
CargoDiscardApplication application = pendingApplication();
storedApplication.set(application);
CargoDiscardItem item = item();
storedItems.add(item);
storedLinks.add(link(item.getId(), 1L));
when(applications.lockNo("100", "FH-1")).thenReturn(application);
when(inventory.changeCartonStatus("100", 1L, "DISCARD_PENDING", "AVAILABLE")).thenReturn(1);
var detail = service.review("100", "FH-1", "9", new CargoDiscardRequests.Review("REJECT", "not damaged"));
assertEquals(CargoDiscardService.REJECTED, detail.summary().status());
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
verify(adjustments, never()).discard(any(), any(), any(), any(), any(), any());
}
private LastlegOrder order() {
LastlegOrder order = new LastlegOrder();
order.setId(101L); order.setCustomerId("100"); order.setOrderType("ONE_CLICK_DROPSHIP");
order.setInboundCode("RK-1"); order.setOverseasWarehouseId("WH-1"); order.setOverseasWarehouseName("LA");
return order;
}
private DropshipSkuInventory skuInventory() {
DropshipSkuInventory value = new DropshipSkuInventory();
value.setId(301L); value.setCustomerId("100"); value.setOrderId(101L); value.setProfileId(201L);
value.setSkuCode("SKU-1"); value.setProductName("Box"); value.setPackageCount(3L);
return value;
}
private DropshipCarton carton(Long id) {
DropshipCarton value = new DropshipCarton();
value.setId(id); value.setCustomerId("100"); value.setOrderId(101L); value.setProfileId(201L);
value.setSkuCode("SKU-1"); value.setCartonMark("C-" + id); value.setStatus("DISCARD_PENDING");
return value;
}
private CargoDiscardApplication pendingApplication() {
CargoDiscardApplication value = new CargoDiscardApplication();
value.setId(401L); value.setCustomerId("100"); value.setDiscardNo("FH-1"); value.setInboundCodes("RK-1");
value.setOverseasWarehouseId("WH-1"); value.setTotalPackages(2); value.setFeePerPackage(new BigDecimal("5.00"));
value.setTotalFee(new BigDecimal("10.00")); value.setCurrency("USD"); value.setStatus(CargoDiscardService.PENDING_REVIEW);
return value;
}
private CargoDiscardItem item() {
CargoDiscardItem value = new CargoDiscardItem();
value.setId(501L); value.setApplicationId(401L); value.setCustomerId("100"); value.setOrderId(101L);
value.setInboundCode("RK-1"); value.setProfileId(201L); value.setSkuInventoryId(301L);
value.setSkuCode("SKU-1"); value.setAvailablePackagesSnapshot(3); value.setDiscardPackages(2);
return value;
}
private CargoDiscardCarton link(Long itemId, Long cartonId) {
CargoDiscardCarton value = new CargoDiscardCarton();
value.setId(600L + cartonId); value.setApplicationId(401L); value.setItemId(itemId);
value.setCustomerId("100"); value.setCartonId(cartonId); value.setCartonMark("C-" + cartonId);
return value;
}
}