派车提交
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.lastleg.dispatch;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DispatchSchemaContractTest {
|
||||
@Test
|
||||
void baselineDefinesDispatchHeadersItemsAndActivePalletLock() throws Exception {
|
||||
String sql = Files.readString(Path.of("src/main/resources/db/lastleg.sql"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_order"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_item"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock"));
|
||||
assertTrue(sql.contains("uk_ll_dispatch_active_pallet"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pda_task"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pda_pick"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pda_operation"));
|
||||
assertTrue(sql.contains("active_key"));
|
||||
assertTrue(sql.contains("order_refs"));
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.baiyue.lastleg.dispatch.pda.service;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper;
|
||||
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaRequests;
|
||||
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaResponses;
|
||||
import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaOperation;
|
||||
import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaOperationMapper;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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 org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DispatchPdaWorkflowServiceTest {
|
||||
@Mock private DispatchOrderMapper orders;
|
||||
@Mock private DispatchPdaOperationMapper operations;
|
||||
|
||||
@Test
|
||||
void pushRequiresVehiclePlateBeforeCreatingPdaTask() {
|
||||
DispatchPdaWorkflowService service = new DispatchPdaWorkflowService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
DispatchOrder order = new DispatchOrder();
|
||||
order.setId(1L); order.setTenantId("100"); order.setDispatchNo("PC1"); order.setVehiclePlate(" ");
|
||||
when(orders.findByDispatchNo("100", "PC1")).thenReturn(order);
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.push("100", "PC1", new DispatchPdaRequests.Push("push-1", null, null)));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void departRequiresVehiclePlate() {
|
||||
DispatchPdaWorkflowService service = new DispatchPdaWorkflowService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "operations", operations);
|
||||
DispatchOrder order = new DispatchOrder();
|
||||
order.setId(1L); order.setTenantId("100"); order.setDispatchNo("PC1");
|
||||
order.setStatus("READY_TO_DEPART"); order.setVehiclePlate(" ");
|
||||
when(orders.findByDispatchNo("100", "PC1")).thenReturn(order);
|
||||
when(orders.lockById("100", 1L)).thenReturn(order);
|
||||
when(operations.findKey("100", "depart-1")).thenReturn(null);
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.depart("100", "PC1", "operator", new DispatchPdaRequests.Depart("depart-1", null)));
|
||||
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void departedRequestReplaysAfterStatusChanged() throws Exception {
|
||||
DispatchPdaWorkflowService service = new DispatchPdaWorkflowService();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "operations", operations);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", objectMapper);
|
||||
DispatchOrder order = new DispatchOrder();
|
||||
order.setId(1L); order.setTenantId("100"); order.setDispatchNo("PC1"); order.setStatus("DEPARTED");
|
||||
when(orders.findByDispatchNo("100", "PC1")).thenReturn(order);
|
||||
when(orders.lockById("100", 1L)).thenReturn(order);
|
||||
DispatchPdaOperation operation = new DispatchPdaOperation();
|
||||
operation.setOperationType("DEPART"); operation.setStatus("COMPLETED");
|
||||
operation.setResultJson(objectMapper.writeValueAsString(new DispatchPdaResponses.Departed("PC1", "DEPARTED", 12, false)));
|
||||
when(operations.findKey("100", "depart-1")).thenReturn(operation);
|
||||
|
||||
// This exact key/hash pair models the persisted first execution result.
|
||||
DispatchPdaRequests.Depart request = new DispatchPdaRequests.Depart("depart-1", null);
|
||||
operation.setRequestHash(java.util.HexFormat.of().formatHex(java.security.MessageDigest.getInstance("SHA-256")
|
||||
.digest(objectMapper.writeValueAsString(request).getBytes(java.nio.charset.StandardCharsets.UTF_8))));
|
||||
DispatchPdaResponses.Departed result = service.depart("100", "PC1", "operator", request);
|
||||
|
||||
assertEquals("DEPARTED", result.status());
|
||||
assertEquals(true, result.replayed());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchPalletLock;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper;
|
||||
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
|
||||
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.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DispatchServiceTest {
|
||||
@Mock DispatchOrderMapper orders;
|
||||
@Mock DispatchItemMapper items;
|
||||
@Mock DispatchPalletLockMapper locks;
|
||||
@Mock DispatchCandidateMapper candidates;
|
||||
private DispatchService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new DispatchService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
ReflectionTestUtils.setField(service, "locks", locks);
|
||||
ReflectionTestUtils.setField(service, "candidates", candidates);
|
||||
lenient().when(items.findByDispatchOrder(any(), any())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preArrangeLocksPalletWithoutVehicleCapacityValidation() throws Exception {
|
||||
assertNotNull(DispatchService.class.getMethod("preArrange", String.class, String.class,
|
||||
DispatchRequests.PreArrange.class).getAnnotation(Transactional.class));
|
||||
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
||||
|
||||
var result = service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null,
|
||||
null, null, null));
|
||||
|
||||
assertEquals(DispatchService.PENDING_DEPARTURE, result.status());
|
||||
assertEquals("ONT8", result.route());
|
||||
ArgumentCaptor<DispatchOrder> dispatch = ArgumentCaptor.forClass(DispatchOrder.class);
|
||||
verify(orders).insert(dispatch.capture());
|
||||
assertEquals(1, dispatch.getValue().getPalletCount());
|
||||
verify(locks).insert(any(DispatchPalletLock.class));
|
||||
verify(items).insert(any(DispatchItem.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsPalletAlreadyLockedByAnotherDispatch() {
|
||||
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
||||
DispatchPalletLock locked = new DispatchPalletLock();
|
||||
when(locks.lockActive(eq("100"), eq("PLAN:10"))).thenReturn(locked);
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.preArrange("100", "7",
|
||||
new DispatchRequests.PreArrange(List.of(10L), null, null, null, null)));
|
||||
verify(orders, never()).insert(any(DispatchOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidCandidateStageIsRejected() {
|
||||
assertThrows(ResponseStatusException.class, () -> service.candidates("100", null, null, "INVALID"));
|
||||
verify(candidates, never()).available(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
private DispatchPalletCandidate candidate(long planPalletId, Long warehousePalletId, String destination) {
|
||||
return new DispatchPalletCandidate(1L, planPalletId, warehousePalletId, 2L, "LMO1", "LMO1", "RK1",
|
||||
"LIP1-P001", destination, destination, destination, destination, 12,
|
||||
new BigDecimal("1.2000"), warehousePalletId == null ? "PLAN" : "RECEIVED");
|
||||
}
|
||||
}
|
||||
+4
@@ -22,6 +22,10 @@ class InboundPlanSchemaContractTest {
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_pda_task"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_pda_operation"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_pallet_merge"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment"));
|
||||
assertTrue(schema.contains("unique key uk_ll_fba_appointment_no (tenant_id, appointment_no)"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment_item"));
|
||||
assertTrue(schema.contains("fba_warehouse_code"));
|
||||
assertFalse(schema.contains("cabinet_no"));
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -29,6 +29,7 @@ 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.atLeastOnce;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -39,6 +40,7 @@ import static org.mockito.Mockito.when;
|
||||
class InboundPdaWorkflowServiceTest {
|
||||
@Mock PdaWorkflowRepository repository;
|
||||
@Mock WarehouseResourceGateway warehouse;
|
||||
@Mock InboundStockReceiptService stockReceipts;
|
||||
private InboundPdaWorkflowService service;
|
||||
|
||||
private InboundPlan plan;
|
||||
@@ -57,6 +59,7 @@ class InboundPdaWorkflowServiceTest {
|
||||
service = new InboundPdaWorkflowService();
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
ReflectionTestUtils.setField(service, "warehouse", warehouse);
|
||||
ReflectionTestUtils.setField(service, "stockReceipts", stockReceipts);
|
||||
ReflectionTestUtils.setField(service, "sourceObjectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
service.initializeObjectMapper();
|
||||
plan = plan();
|
||||
@@ -249,6 +252,20 @@ class InboundPdaWorkflowServiceTest {
|
||||
assertEquals("COMPLETED", plan.getStatus());
|
||||
verify(repository).update(task);
|
||||
verify(repository).update(plan);
|
||||
verify(stockReceipts).stockIn(eq("100"), eq(plan), eq(task), eq(List.of(targetLine)),
|
||||
eq(List.of(target, moved)), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedTaskReplayDoesNotRunStockInAgain() {
|
||||
task.setStatus("COMPLETED");
|
||||
activeTask();
|
||||
|
||||
var result = service.complete("100", "LIP1");
|
||||
|
||||
assertEquals("COMPLETED", result.status());
|
||||
verify(stockReceipts, never()).stockIn(any(), any(), any(), any(), any(), any(), any());
|
||||
verify(repository, never()).allocations(plan.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.service;
|
||||
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundStockReceipt;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundStockReceiptMapper;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
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.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundStockReceiptServiceTest {
|
||||
@Mock LastlegOrderMapper orders;
|
||||
@Mock LastlegOrderDetailMapper details;
|
||||
@Mock InboundStockReceiptMapper receipts;
|
||||
@Mock InventoryCommandService inventory;
|
||||
private InboundStockReceiptService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InboundStockReceiptService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "details", details);
|
||||
ReflectionTestUtils.setField(service, "receipts", receipts);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
when(receipts.listTask("100", 2L)).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void writesOneReceiptPerSkuAndPalletAndUsesPdaPackageCount() {
|
||||
Fixture fixture = fixture(5, 5, 5, 10);
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(details.selectList(any())).thenReturn(List.of(fixture.detail));
|
||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
|
||||
|
||||
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
|
||||
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now());
|
||||
|
||||
ArgumentCaptor<InventoryCommandRequests.StockIn> request = ArgumentCaptor.forClass(InventoryCommandRequests.StockIn.class);
|
||||
verify(inventory).stockIn(eq("100"), eq("operator"), request.capture());
|
||||
assertEquals(5L, request.getValue().packages());
|
||||
assertEquals(10L, request.getValue().productQuantity());
|
||||
assertEquals("SKU-1", request.getValue().skuCode());
|
||||
ArgumentCaptor<InboundStockReceipt> receipt = ArgumentCaptor.forClass(InboundStockReceipt.class);
|
||||
verify(receipts).insert(receipt.capture());
|
||||
assertEquals(5L, receipt.getValue().getReceivedPackages());
|
||||
assertEquals(10L, receipt.getValue().getReceivedQuantity());
|
||||
assertEquals(10L, receipt.getValue().getInventoryPositionId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMultiSkuLineSplitAcrossPalletsInsteadOfProportionalMisallocation() {
|
||||
Fixture fixture = fixture(10, 5, 5, 10);
|
||||
fixture.detail.setPackageCount(5);
|
||||
LastlegOrderDetail second = detail(202L, "SKU-2", 5, 7);
|
||||
InboundPlanPallet secondPallet = pallet(22L, 2002L);
|
||||
InboundPlanPalletAllocation secondAllocation = allocation(32L, 12L, 22L, 5);
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(details.selectList(any())).thenReturn(List.of(fixture.detail, second));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.stockIn("100", fixture.plan, fixture.task,
|
||||
List.of(fixture.line), List.of(fixture.allocation, secondAllocation),
|
||||
List.of(fixture.pallet, secondPallet), LocalDateTime.now()));
|
||||
verify(inventory, never()).stockIn(any(), any(), any());
|
||||
verify(receipts, never()).insert(any(InboundStockReceipt.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void conservesSingleSkuPackagesAndQuantityAcrossPallets() {
|
||||
Fixture fixture = fixture(5, 2, 2, 11);
|
||||
InboundPlanPallet secondPallet = pallet(22L, 2002L);
|
||||
InboundPlanPalletAllocation secondAllocation = allocation(32L, 12L, 22L, 3);
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(details.selectList(any())).thenReturn(List.of(fixture.detail));
|
||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
|
||||
|
||||
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
|
||||
List.of(fixture.allocation, secondAllocation), List.of(fixture.pallet, secondPallet), LocalDateTime.now());
|
||||
|
||||
ArgumentCaptor<InventoryCommandRequests.StockIn> requests = ArgumentCaptor.forClass(InventoryCommandRequests.StockIn.class);
|
||||
verify(inventory, times(2)).stockIn(eq("100"), eq("operator"), requests.capture());
|
||||
assertEquals(5L, requests.getAllValues().stream().mapToLong(InventoryCommandRequests.StockIn::packages).sum());
|
||||
assertEquals(11L, requests.getAllValues().stream().mapToLong(InventoryCommandRequests.StockIn::productQuantity).sum());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingTaskReceiptMakesStockInReplayANoOp() {
|
||||
Fixture fixture = fixture(5, 5, 5, 10);
|
||||
when(receipts.listTask("100", fixture.task.getId())).thenReturn(List.of(new InboundStockReceipt()));
|
||||
|
||||
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
|
||||
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now());
|
||||
|
||||
verify(inventory, never()).stockIn(any(), any(), any());
|
||||
verify(receipts, never()).insert(any(InboundStockReceipt.class));
|
||||
}
|
||||
|
||||
private Fixture fixture(int required, int planned, int picked, int quantity) {
|
||||
InboundPlan plan = new InboundPlan(); plan.setId(1L); plan.setPlanNo("IP-1");
|
||||
InboundPdaTask task = new InboundPdaTask(); task.setId(2L); task.setOperatorId("operator");
|
||||
InboundPlanLine line = new InboundPlanLine(); line.setId(12L); line.setPlanId(1L); line.setOrderId(101L);
|
||||
line.setInboundCode("IN-1"); line.setDestinationWarehouseId("WH-DEST");
|
||||
line.setDestinationWarehouseCode("ONT8"); line.setDestinationWarehouseName("ONT8");
|
||||
line.setRequiredPackageCount(required); line.setVolumeCbm(new BigDecimal("1.0"));
|
||||
LastlegOrder order = new LastlegOrder(); order.setId(101L); order.setTenantId("100");
|
||||
order.setOrderNo("LMO-1"); order.setCustomerName("Customer"); order.setOverseasWarehouseId("OW-1");
|
||||
order.setOverseasWarehouseName("Warehouse"); order.setSourceEventId("evt-1"); order.setFbaWarehouseCode("ONT8");
|
||||
LastlegOrderDetail detail = detail(201L, "SKU-1", required, quantity);
|
||||
return new Fixture(plan, task, line, order, detail, allocation(31L, 12L, 21L, picked), pallet(21L, 2001L));
|
||||
}
|
||||
|
||||
private LastlegOrderDetail detail(long id, String sku, int packages, int quantity) {
|
||||
LastlegOrderDetail detail = new LastlegOrderDetail(); detail.setId(id); detail.setOrderId(101L);
|
||||
detail.setSkuCode(sku); detail.setPackageCount(packages); detail.setProductQuantity(quantity);
|
||||
detail.setProductNameZh(sku + " name"); return detail;
|
||||
}
|
||||
|
||||
private InboundPlanPallet pallet(long id, long warehousePalletId) {
|
||||
InboundPlanPallet pallet = new InboundPlanPallet(); pallet.setId(id); pallet.setPlanId(1L);
|
||||
pallet.setPalletNo("PLT-" + id); pallet.setWarehousePalletId(warehousePalletId);
|
||||
pallet.setWarehouseLocationId(3001L); return pallet;
|
||||
}
|
||||
|
||||
private InboundPlanPalletAllocation allocation(long id, long lineId, long palletId, int picked) {
|
||||
InboundPlanPalletAllocation allocation = new InboundPlanPalletAllocation(); allocation.setId(id);
|
||||
allocation.setPlanId(1L); allocation.setLineId(lineId); allocation.setPalletId(palletId);
|
||||
allocation.setPlannedPackageCount(picked); allocation.setPickedPackageCount(picked); return allocation;
|
||||
}
|
||||
|
||||
private record Fixture(InboundPlan plan, InboundPdaTask task, InboundPlanLine line, LastlegOrder order,
|
||||
LastlegOrderDetail detail, InboundPlanPalletAllocation allocation,
|
||||
InboundPlanPallet pallet) {}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package com.baiyue.lastleg.inboundplan.service;
|
||||
|
||||
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointment;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointmentItem;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentItemMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentQueryMapper;
|
||||
import com.baiyue.lastleg.inboundplan.model.FbaAppointmentLineSnapshot;
|
||||
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 org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundFbaAppointmentServiceTest {
|
||||
@Mock InboundFbaAppointmentMapper appointmentMapper;
|
||||
@Mock InboundFbaAppointmentItemMapper itemMapper;
|
||||
@Mock InboundFbaAppointmentQueryMapper queryMapper;
|
||||
|
||||
private InboundFbaAppointmentService service;
|
||||
private final LocalDateTime appointmentAt = LocalDateTime.of(2026, 8, 20, 10, 30);
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InboundFbaAppointmentService();
|
||||
ReflectionTestUtils.setField(service, "appointmentMapper", appointmentMapper);
|
||||
ReflectionTestUtils.setField(service, "itemMapper", itemMapper);
|
||||
ReflectionTestUtils.setField(service, "queryMapper", queryMapper);
|
||||
lenient().when(itemMapper.selectList(any())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsOneBatchForSelectedLinesAndFreezesFbaWarehouse() throws Exception {
|
||||
assertEquals(true, InboundFbaAppointmentService.class
|
||||
.getMethod("batchCreate", String.class, String.class, BatchFbaAppointmentRequest.class)
|
||||
.getAnnotation(Transactional.class) != null);
|
||||
when(queryMapper.lockLines("100", List.of(11L, 12L))).thenReturn(List.of(
|
||||
line(11L, "IN-11", "ONT8"), line(12L, "IN-12", "ONT8")));
|
||||
|
||||
var result = service.batchCreate("100", "7", request(List.of(11L, 12L), "APT-1"));
|
||||
|
||||
assertEquals("APT-1", result.appointmentNo());
|
||||
assertEquals("ONT8", result.fbaWarehouseCode());
|
||||
assertEquals(2, result.selectedCount());
|
||||
verify(appointmentMapper).insert(any(InboundFbaAppointment.class));
|
||||
verify(itemMapper, org.mockito.Mockito.times(2)).insert(any(InboundFbaAppointmentItem.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMixedFbaWarehouses() {
|
||||
when(queryMapper.lockLines("100", List.of(11L, 12L))).thenReturn(List.of(
|
||||
line(11L, "IN-11", "ONT8"), line(12L, "IN-12", "LAX9")));
|
||||
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> service.batchCreate("100", "7", request(List.of(11L, 12L), "APT-1")));
|
||||
verify(appointmentMapper, never()).insert(any(InboundFbaAppointment.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingLineFromAnotherTenant() {
|
||||
when(queryMapper.lockLines("100", List.of(11L, 12L))).thenReturn(List.of(line(11L, "IN-11", "ONT8")));
|
||||
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> service.batchCreate("100", "7", request(List.of(11L, 12L), "APT-1")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateLineIdsAreRejectedBeforeDatabaseAccess() {
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> service.batchCreate("100", "7", request(List.of(11L, 11L), "APT-1")));
|
||||
verify(queryMapper, never()).lockLines(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameAppointmentReplayReturnsExistingResult() {
|
||||
InboundFbaAppointment existing = appointment(99L, "APT-1", "ONT8");
|
||||
InboundFbaAppointmentItem item = new InboundFbaAppointmentItem();
|
||||
item.setLineId(11L);
|
||||
item.setInboundCode("IN-11");
|
||||
item.setFbaWarehouseCode("ONT8");
|
||||
when(appointmentMapper.selectOne(any())).thenReturn(existing);
|
||||
when(itemMapper.selectList(any())).thenReturn(List.of(item));
|
||||
|
||||
var result = service.batchCreate("100", "7", request(List.of(11L), "APT-1"));
|
||||
|
||||
assertEquals(99L, result.id());
|
||||
verify(appointmentMapper, never()).insert(any(InboundFbaAppointment.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameAppointmentNumberWithDifferentSelectionConflicts() {
|
||||
when(appointmentMapper.selectOne(any())).thenReturn(appointment(99L, "APT-1", "ONT8"));
|
||||
InboundFbaAppointmentItem item = new InboundFbaAppointmentItem();
|
||||
item.setLineId(11L);
|
||||
when(itemMapper.selectList(any())).thenReturn(List.of(item));
|
||||
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> service.batchCreate("100", "7", request(List.of(12L), "APT-1")));
|
||||
}
|
||||
|
||||
private BatchFbaAppointmentRequest request(List<Long> lineIds, String number) {
|
||||
return new BatchFbaAppointmentRequest("100", lineIds, number, appointmentAt, "SUCCESS", "dock");
|
||||
}
|
||||
|
||||
private FbaAppointmentLineSnapshot line(Long id, String inboundCode, String fba) {
|
||||
return new FbaAppointmentLineSnapshot(id, 1000L, id + 100L, inboundCode, fba);
|
||||
}
|
||||
|
||||
private InboundFbaAppointment appointment(Long id, String number, String fba) {
|
||||
InboundFbaAppointment value = new InboundFbaAppointment();
|
||||
value.setId(id);
|
||||
value.setTenantId("100");
|
||||
value.setAppointmentNo(number);
|
||||
value.setAppointmentAt(appointmentAt);
|
||||
value.setAppointmentStatus("SUCCESS");
|
||||
value.setAppointmentRemark("dock");
|
||||
value.setFbaWarehouseCode(fba);
|
||||
value.setSelectedCount(1);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.baiyue.lastleg.inventory;
|
||||
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryLedger;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryOperation;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class InventorySchemaContractTest {
|
||||
@Test
|
||||
void baselineDefinesInventoryDimensionsStatesAndIdempotency() throws IOException {
|
||||
String schema;
|
||||
try (var stream = getClass().getClassLoader().getResourceAsStream("db/lastleg.sql")) {
|
||||
assertNotNull(stream, "lastleg schema must be available");
|
||||
schema = new String(stream.readAllBytes(), StandardCharsets.UTF_8).toLowerCase();
|
||||
}
|
||||
|
||||
assertTrue(schema.contains("create table if not exists ll_inventory_balance"));
|
||||
assertTrue(schema.contains("unique key uk_ll_inventory_dimension (tenant_id,overseas_warehouse_id,destination_warehouse_code,sku_code)"));
|
||||
assertTrue(schema.contains("available_packages bigint not null default 0"));
|
||||
assertTrue(schema.contains("in_transit_packages bigint not null default 0"));
|
||||
assertTrue(schema.contains("procurement_quantity bigint not null default 0"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inventory_position"));
|
||||
assertTrue(schema.contains("unique key uk_ll_inventory_position (balance_id,pallet_id,batch_no)"));
|
||||
assertTrue(schema.contains("warehouse_location_code varchar(128) not null"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inventory_ledger"));
|
||||
assertTrue(schema.contains("business_order_id bigint null"));
|
||||
assertTrue(schema.contains("inbound_code varchar(128) null"));
|
||||
assertTrue(schema.contains("before_packages bigint not null"));
|
||||
assertTrue(schema.contains("delta_packages bigint not null"));
|
||||
assertTrue(schema.contains("after_packages bigint not null"));
|
||||
assertTrue(schema.contains("unique key uk_ll_inventory_ledger_idempotency (tenant_id,idempotency_key)"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inventory_operation"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_stock_receipt"));
|
||||
assertTrue(schema.contains("unique key uk_ll_stock_receipt_task_allocation_sku (tenant_id,task_id,allocation_id,sku_code)"));
|
||||
assertFalse(tableDefinition(schema, "ll_inventory_ledger").contains("updated_at"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void inventoryEntitiesUseSnowflakeIds() throws NoSuchFieldException {
|
||||
for (Class<?> type : new Class<?>[] {
|
||||
InventoryBalance.class, InventoryPosition.class, InventoryLedger.class, InventoryOperation.class
|
||||
}) {
|
||||
TableId tableId = type.getDeclaredField("id").getAnnotation(TableId.class);
|
||||
assertNotNull(tableId, type.getSimpleName());
|
||||
assertEquals(IdType.ASSIGN_ID, tableId.type(), type.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void incrementalMigrationIsAdditiveAndRerunnable() throws IOException {
|
||||
String migration = Files.readString(Path.of("..", "scripts", "db", "incremental.sql"), StandardCharsets.UTF_8)
|
||||
.toLowerCase();
|
||||
assertTrue(migration.contains("2026-08-17 / 008"));
|
||||
assertTrue(migration.contains("create table if not exists ll_inventory_balance"));
|
||||
assertTrue(migration.contains("create table if not exists ll_inventory_position"));
|
||||
assertTrue(migration.contains("create table if not exists ll_inventory_ledger"));
|
||||
assertTrue(migration.contains("create table if not exists ll_inventory_operation"));
|
||||
}
|
||||
|
||||
private static String tableDefinition(String schema, String tableName) {
|
||||
int start = schema.indexOf("create table if not exists " + tableName);
|
||||
int end = schema.indexOf("engine=innodb", start);
|
||||
return schema.substring(start, end);
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
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.mapper.InventoryCommandMapper;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryLedger;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryOperation;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryLedgerMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryOperationMapper;
|
||||
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
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 org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
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.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InventoryCommandServiceTest {
|
||||
@Mock InventoryBalanceMapper balances;
|
||||
@Mock InventoryPositionMapper positions;
|
||||
@Mock InventoryLedgerMapper ledgers;
|
||||
@Mock InventoryOperationMapper operations;
|
||||
@Mock InventoryCommandMapper commands;
|
||||
@Mock WarehouseResourceGateway warehouseGateway;
|
||||
|
||||
private InventoryCommandService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InventoryCommandService();
|
||||
ReflectionTestUtils.setField(service, "balances", balances);
|
||||
ReflectionTestUtils.setField(service, "positions", positions);
|
||||
ReflectionTestUtils.setField(service, "ledgers", ledgers);
|
||||
ReflectionTestUtils.setField(service, "operations", operations);
|
||||
ReflectionTestUtils.setField(service, "commands", commands);
|
||||
ReflectionTestUtils.setField(service, "warehouseGateway", warehouseGateway);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
lenient().when(operations.findIdempotency(any(), any())).thenReturn(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stockInUpdatesBalancePositionAndLedgerInOneTransaction() throws Exception {
|
||||
assertTrue(InventoryCommandService.class.getMethod("stockIn", String.class, String.class,
|
||||
InventoryCommandRequests.StockIn.class).getAnnotation(Transactional.class) != null);
|
||||
when(balances.lockDimension("100", "WH-1", "ONT8", "SKU-A")).thenReturn(null);
|
||||
var result = service.stockIn("100", "7", stockIn("IN-1", "stock-in-1", 12, 24));
|
||||
|
||||
assertEquals(12, result.availablePackages());
|
||||
assertFalse(result.replayed());
|
||||
ArgumentCaptor<InventoryBalance> balance = ArgumentCaptor.forClass(InventoryBalance.class);
|
||||
verify(balances).updateById(balance.capture());
|
||||
assertEquals(12L, balance.getValue().getAvailablePackages());
|
||||
verify(positions).insert(any(InventoryPosition.class));
|
||||
verify(ledgers).insert(any(InventoryLedger.class));
|
||||
verify(operations).updateById(any(InventoryOperation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void holdRejectsWhenAvailablePackagesAreInsufficient() {
|
||||
InventoryBalance balance = balance(200L, 5);
|
||||
InventoryPosition position = position(201L, 200L, 5);
|
||||
when(commands.lockPosition("100", 201L)).thenReturn(position);
|
||||
when(commands.lockBalance("100", 200L)).thenReturn(balance);
|
||||
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> service.hold("100", "7", new InventoryCommandRequests.Move(
|
||||
"100", 201L, 6, 0, "ORD-1", null, null, "hold-1", null)));
|
||||
verify(positions, never()).updateById(any(InventoryPosition.class));
|
||||
verify(balances, never()).updateById(any(InventoryBalance.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void replayUsesPersistedResultWithoutMutatingInventoryAgain() throws Exception {
|
||||
var request = stockIn("IN-1", "stock-in-1", 12, 24);
|
||||
when(balances.lockDimension("100", "WH-1", "ONT8", "SKU-A")).thenReturn(null);
|
||||
service.stockIn("100", "7", request);
|
||||
ArgumentCaptor<InventoryOperation> operation = ArgumentCaptor.forClass(InventoryOperation.class);
|
||||
verify(operations).insert(operation.capture());
|
||||
when(operations.findIdempotency("100", "stock-in-1")).thenReturn(operation.getValue());
|
||||
|
||||
InventoryCommandResponse replay = service.stockIn("100", "7", request);
|
||||
|
||||
assertTrue(replay.replayed());
|
||||
verify(balances, times(1)).updateById(any(InventoryBalance.class));
|
||||
verify(ledgers, times(1)).insert(any(InventoryLedger.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shippingLastPackagesScrapsPalletWithoutReleasingItsVirtualLocation() {
|
||||
InventoryBalance balance = balance(200L, 0);
|
||||
balance.setPickedPackages(5L);
|
||||
balance.setPickedQuantity(5L);
|
||||
InventoryPosition position = position(201L, 200L, 0);
|
||||
position.setPickedPackages(5L);
|
||||
position.setProductQuantity(5L);
|
||||
when(commands.lockPosition("100", 201L)).thenReturn(position);
|
||||
when(commands.lockBalance("100", 200L)).thenReturn(balance);
|
||||
when(commands.palletPackages("100", 300L)).thenReturn(0L);
|
||||
|
||||
var result = service.ship("100", "7", new InventoryCommandRequests.Move(
|
||||
"100", 201L, 5, 5, "OUT-1", "OUT-1", null, "ship-1", null));
|
||||
|
||||
assertTrue(result.palletEmpty());
|
||||
assertEquals("EMPTY", position.getStatus());
|
||||
verify(warehouseGateway).scrapPallet("100", 300L);
|
||||
verify(ledgers).insert(any(InventoryLedger.class));
|
||||
}
|
||||
|
||||
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", "SKU-A", "Widget", 300L, "PLT-1", null, null, null,
|
||||
packages, quantity, "ORD-1", inbound, "EVT-1", key, null);
|
||||
}
|
||||
|
||||
private InventoryBalance balance(Long id, long available) {
|
||||
InventoryBalance value = new InventoryBalance();
|
||||
value.setId(id); value.setTenantId("100"); value.setOverseasWarehouseId("WH-1");
|
||||
value.setDestinationWarehouseCode("ONT8"); value.setSkuCode("SKU-A");
|
||||
value.setAvailablePackages(available); value.setAvailableQuantity(available);
|
||||
value.setHoldPackages(0L); value.setAllocatedPackages(0L); value.setPickedPackages(0L);
|
||||
value.setHoldQuantity(0L); value.setAllocatedQuantity(0L); value.setPickedQuantity(0L);
|
||||
value.setVersion(0L);
|
||||
return value;
|
||||
}
|
||||
|
||||
private InventoryPosition position(Long id, Long balanceId, long available) {
|
||||
InventoryPosition value = new InventoryPosition();
|
||||
value.setId(id); value.setTenantId("100"); value.setBalanceId(balanceId);
|
||||
value.setPalletId(300L); value.setPalletNo("PLT-1"); value.setBatchNo("DEFAULT");
|
||||
value.setAvailablePackages(available); value.setHoldPackages(0L);
|
||||
value.setAllocatedPackages(0L); value.setPickedPackages(0L); value.setReceivingPackages(0L);
|
||||
value.setProductQuantity(available); value.setVersion(0L);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.baiyue.lastleg.inventory.query.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.inventory.query.dto.InventoryQueryRequests;
|
||||
import com.baiyue.lastleg.inventory.query.service.InventoryQueryService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class InventoryQueryControllerTest {
|
||||
private final InventoryQueryService service = mock(InventoryQueryService.class);
|
||||
private final InventoryQueryController controller = controller();
|
||||
private final InventoryQueryRequests.BalanceQuery query = new InventoryQueryRequests.BalanceQuery(
|
||||
1, 20, null, null, null, null, null, null, null);
|
||||
|
||||
@Test
|
||||
void merchantCannotOverrideCustomerScope() {
|
||||
controller.balances(user(100L, "MERCHANT"), "200", query);
|
||||
verify(service).balances(eq("100"), eq(query));
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformMustSelectCustomer() {
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> controller.summary(user(0L, "PLATFORM"), null, query));
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformCanQuerySelectedCustomer() {
|
||||
controller.summary(user(0L, "PLATFORM"), "200", query);
|
||||
verify(service).dashboard(eq("200"), eq(query));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unauthenticatedPositionDetailIsRejected() {
|
||||
InventoryQueryRequests.PositionQuery positionQuery =
|
||||
new InventoryQueryRequests.PositionQuery(null, null, null);
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> controller.positions(null, null, 1L, positionQuery));
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(long tenantId, String appCode) {
|
||||
return new AuthenticatedUser(1L, tenantId, tenantId, 1L, "tester", appCode, "WEB", Set.of());
|
||||
}
|
||||
|
||||
private InventoryQueryController controller() {
|
||||
InventoryQueryController controller = new InventoryQueryController();
|
||||
ReflectionTestUtils.setField(controller, "service", service);
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package com.baiyue.lastleg.inventory.query.service;
|
||||
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
import com.baiyue.lastleg.inventory.query.dto.InventoryDashboardResponse;
|
||||
import com.baiyue.lastleg.inventory.query.dto.InventoryQueryRequests;
|
||||
import com.baiyue.lastleg.inventory.query.mapper.InventoryQueryMapper;
|
||||
import com.baiyue.lastleg.inventory.query.model.InventoryAggregateProjection;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class InventoryQueryServiceTest {
|
||||
private final InventoryQueryMapper mapper = mock(InventoryQueryMapper.class);
|
||||
private final InventoryQueryService service = new InventoryQueryService();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(service, "mapper", mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void dashboardUsesSameTenantAndFiltersForTotalsAndPalletCount() {
|
||||
InventoryQueryRequests.BalanceQuery query = balanceQuery("AVAILABLE");
|
||||
InventoryAggregateProjection aggregate = new InventoryAggregateProjection();
|
||||
aggregate.setDimensionCount(2L);
|
||||
aggregate.setAvailablePackages(12L);
|
||||
aggregate.setAvailableQuantity(120L);
|
||||
when(mapper.aggregate("100", query)).thenReturn(aggregate);
|
||||
when(mapper.countPallets("100", query)).thenReturn(3L);
|
||||
|
||||
InventoryDashboardResponse result = service.dashboard("100", query);
|
||||
|
||||
assertEquals(2, result.skuDimensionCount());
|
||||
assertEquals(3, result.palletCount());
|
||||
assertEquals(12, result.totalPackages());
|
||||
assertEquals(120, result.totalQuantity());
|
||||
}
|
||||
|
||||
@Test
|
||||
void balancePageIsBoundToRequestedTenant() {
|
||||
InventoryQueryRequests.BalanceQuery query = balanceQuery(null);
|
||||
InventoryBalance balance = new InventoryBalance();
|
||||
balance.setId(1L);
|
||||
balance.setTenantId("100");
|
||||
when(mapper.countBalances("100", query)).thenReturn(1L);
|
||||
when(mapper.balances("100", query, 0, 20)).thenReturn(List.of(balance));
|
||||
|
||||
service.balances("100", query);
|
||||
|
||||
verify(mapper).countBalances("100", query);
|
||||
verify(mapper).balances("100", query, 0, 20);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownStatusIsRejectedBeforeDatabaseQuery() {
|
||||
InventoryQueryRequests.BalanceQuery query = balanceQuery("DAMAGED");
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.balances("100", query));
|
||||
verify(mapper, never()).countBalances(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void positionDetailDoesNotLeakAnotherTenantBalance() {
|
||||
InventoryQueryRequests.PositionQuery query = new InventoryQueryRequests.PositionQuery(null, null, null);
|
||||
when(mapper.countOwnedBalance("100", 99L)).thenReturn(0L);
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.positions("100", 99L, query));
|
||||
verify(mapper, never()).positions(eq("100"), eq(99L), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reversedLedgerDateRangeIsRejected() {
|
||||
InventoryQueryRequests.LedgerQuery query = new InventoryQueryRequests.LedgerQuery(
|
||||
1, 20, null, null, null, null, null, null, null, null, null,
|
||||
java.time.LocalDateTime.of(2026, 8, 18, 0, 0),
|
||||
java.time.LocalDateTime.of(2026, 8, 17, 0, 0));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.ledgers("100", query));
|
||||
verify(mapper, never()).countLedgers(any(), any());
|
||||
}
|
||||
|
||||
private static InventoryQueryRequests.BalanceQuery balanceQuery(String status) {
|
||||
return new InventoryQueryRequests.BalanceQuery(1, 20, null, null, null, null, null, null, status);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user