fix(lastleg): restore returned inventory from shipment source

This commit is contained in:
2026-09-08 18:30:04 +08:00
parent 047ed674b8
commit db98da5689
2 changed files with 334 additions and 2 deletions
@@ -2,6 +2,8 @@ package com.baiyue.lastleg.inventory.command;
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreRequest;
import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreResponse;
import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper;
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
@@ -17,6 +19,7 @@ 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.apache.ibatis.annotations.Select;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -57,7 +60,7 @@ class InventoryCommandServiceTest {
ReflectionTestUtils.setField(service, "operations", operations);
ReflectionTestUtils.setField(service, "commands", commands);
ReflectionTestUtils.setField(service, "warehouseGateway", warehouseGateway);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
lenient().when(operations.findIdempotency(any(), any())).thenReturn(null);
lenient().when(commands.lockInboundTransitBalances(any(), any(), any())).thenReturn(List.of());
}
@@ -329,6 +332,97 @@ class InventoryCommandServiceTest {
verify(operations).findIdempotency("200", "platform-hold-1");
}
@Test
void restoresReturnedInventoryFromImmutableShipmentSourceIntoVerifiedTarget() throws Exception {
assertTrue(InventoryCommandService.class.getMethod("restoreReturnedInventory", String.class, String.class,
ReturnedInventoryRestoreRequest.class).getAnnotation(Transactional.class) != null);
InventoryBalance balance = balance(200L, 2);
balance.setOverseasWarehouseId("900");
InventoryPosition source = position(201L, 200L, 0);
when(commands.lockPosition("100", 201L)).thenReturn(source);
when(commands.lockBalance("100", 200L)).thenReturn(balance);
when(commands.shippedPackages("100", 201L, "OUT-1", "SKU-1")).thenReturn(8L);
when(commands.restoredPackages("100", "RETURN_SOURCE:301:201", "SKU-1")).thenReturn(1L);
when(warehouseGateway.resolvePallet(400L)).thenReturn(new WarehouseResourceGateway.WarehousePalletReference(
400L, "PLT-RETURN", 900L, 500L, "IDLE", null, 0));
when(warehouseGateway.resolveLocation(900L, 500L)).thenReturn(
new WarehouseResourceGateway.WarehouseLocationReference(500L, "LOC-RETURN", "STORAGE", null, "ACTIVE"));
ReturnedInventoryRestoreResponse result = service.restoreReturnedInventory("100", "7", returnRequest(3));
assertEquals(2, result.beforePackages());
assertEquals(3, result.restoredPackages());
assertEquals(5, result.afterPackages());
assertFalse(result.replayed());
ArgumentCaptor<InventoryPosition> target = ArgumentCaptor.forClass(InventoryPosition.class);
verify(positions).insert(target.capture());
assertEquals("RETURN:EX-1", target.getValue().getBatchNo());
assertEquals("LOC-RETURN", target.getValue().getWarehouseLocationCode());
assertEquals(3L, target.getValue().getAvailablePackages());
ArgumentCaptor<InventoryLedger> ledger = ArgumentCaptor.forClass(InventoryLedger.class);
verify(ledgers).insert(ledger.capture());
assertEquals("RETURN_STOCK_IN", ledger.getValue().getBusinessType());
assertEquals("RETURN_SOURCE:301:201", ledger.getValue().getSourceEventId());
assertEquals("OUT-1", ledger.getValue().getOutboundOrderNo());
}
@Test
void returnedInventoryReplayDoesNotMutateInventoryTwice() {
InventoryBalance balance = balance(200L, 0);
balance.setOverseasWarehouseId("900");
InventoryPosition source = position(201L, 200L, 0);
when(commands.lockPosition("100", 201L)).thenReturn(source);
when(commands.lockBalance("100", 200L)).thenReturn(balance);
when(commands.shippedPackages("100", 201L, "OUT-1", "SKU-1")).thenReturn(8L);
when(commands.restoredPackages("100", "RETURN_SOURCE:301:201", "SKU-1")).thenReturn(0L);
when(warehouseGateway.resolvePallet(400L)).thenReturn(new WarehouseResourceGateway.WarehousePalletReference(
400L, "PLT-RETURN", 900L, 500L, "IDLE", null, 0));
when(warehouseGateway.resolveLocation(900L, 500L)).thenReturn(
new WarehouseResourceGateway.WarehouseLocationReference(500L, "LOC-RETURN", "STORAGE", null, "ACTIVE"));
ReturnedInventoryRestoreRequest request = returnRequest(3);
service.restoreReturnedInventory("100", "7", request);
ArgumentCaptor<InventoryOperation> operation = ArgumentCaptor.forClass(InventoryOperation.class);
verify(operations).insert(operation.capture());
when(operations.findIdempotency("100", "return-1")).thenReturn(operation.getValue());
ReturnedInventoryRestoreResponse replay = service.restoreReturnedInventory("100", "7", request);
assertTrue(replay.replayed());
verify(positions, times(1)).insert(any(InventoryPosition.class));
verify(ledgers, times(1)).insert(any(InventoryLedger.class));
}
@Test
void rejectsReturnBeyondRemainingImmutableShipmentWithoutInventoryMutation() {
InventoryBalance balance = balance(200L, 0);
balance.setOverseasWarehouseId("900");
when(commands.lockPosition("100", 201L)).thenReturn(position(201L, 200L, 0));
when(commands.lockBalance("100", 200L)).thenReturn(balance);
when(commands.shippedPackages("100", 201L, "OUT-1", "SKU-1")).thenReturn(4L);
when(commands.restoredPackages("100", "RETURN_SOURCE:301:201", "SKU-1")).thenReturn(2L);
assertThrows(ResponseStatusException.class,
() -> service.restoreReturnedInventory("100", "7", returnRequest(3)));
verify(positions, never()).insert(any(InventoryPosition.class));
verify(positions, never()).updateById(any(InventoryPosition.class));
verify(balances, never()).updateById(any(InventoryBalance.class));
verify(ledgers, never()).insert(any(InventoryLedger.class));
}
@Test
void returnRestoreSqlIsBoundToImmutableShipLedgerAndSourceIdentity() throws Exception {
String shippedSql = String.join(" ", InventoryCommandMapper.class.getMethod("shippedPackages",
String.class, Long.class, String.class, String.class).getAnnotation(Select.class).value());
assertTrue(shippedSql.contains("position_id=#{positionId}"));
assertTrue(shippedSql.contains("outbound_order_no=#{outboundNo}"));
assertTrue(shippedSql.contains("business_type='SHIP'"));
String restoredSql = String.join(" ", InventoryCommandMapper.class.getMethod("restoredPackages",
String.class, String.class, String.class).getAnnotation(Select.class).value());
assertTrue(restoredSql.contains("source_event_id=#{sourceIdentity}"));
assertTrue(restoredSql.contains("business_type='RETURN_STOCK_IN'"));
}
private InventoryCommandRequests.StockIn stockIn(String inbound, String key, long packages, long quantity) {
return new InventoryCommandRequests.StockIn("100", "Customer 100", "WH-1", "WH1", "Warehouse 1",
"DEST-1", "ONT8", "ONT8", 101L, inbound, "IN-CODE-1", "SKU-1", "Product 1", null,
@@ -342,6 +436,14 @@ class InventoryCommandServiceTest {
packages, "ORD-1", "EVT-1", key, null);
}
private ReturnedInventoryRestoreRequest returnRequest(long packages) {
return new ReturnedInventoryRestoreRequest("EX-1", "OUT-1", "900", 101L,
List.of(new ReturnedInventoryRestoreRequest.SourceItem(301L, 201L, "SKU-1", 5)),
List.of(new ReturnedInventoryRestoreRequest.Receipt(301L, 201L, packages,
400L, "PLT-RETURN", 500L, "LOC-RETURN")),
"return-1", List.of("FILE-1"));
}
private InventoryBalance balance(Long id, long available) {
InventoryBalance value = new InventoryBalance();
value.setId(id); value.setCustomerId("100"); value.setCustomerName("Customer 100");