暂存提交
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
||||
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 (customer_id,overseas_warehouse_id,destination_warehouse_code,inbound_order_id,inbound_code)"));
|
||||
assertTrue(schema.contains("inbound_order_id bigint not null"));
|
||||
assertTrue(schema.contains("inbound_code varchar(128) not null"));
|
||||
assertTrue(schema.contains("available_packages bigint not null default 0"));
|
||||
assertTrue(schema.contains("in_transit_packages bigint not null default 0"));
|
||||
assertFalse(tableDefinition(schema, "ll_inventory_balance").contains("sku_code"));
|
||||
assertFalse(tableDefinition(schema, "ll_inventory_balance").contains("_quantity"));
|
||||
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"));
|
||||
assertFalse(tableDefinition(schema, "ll_inventory_position").contains("sku_code"));
|
||||
assertFalse(tableDefinition(schema, "ll_inventory_position").contains("product_quantity"));
|
||||
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(tableDefinition(schema, "ll_inventory_ledger").contains("sku_code"));
|
||||
assertTrue(tableDefinition(schema, "ll_inventory_ledger").contains("carton_mark"));
|
||||
assertFalse(tableDefinition(schema, "ll_inventory_ledger").contains("before_quantity"));
|
||||
assertTrue(schema.contains("unique key uk_ll_inventory_ledger_idempotency (customer_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 (customer_id,task_id,allocation_id)"));
|
||||
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"));
|
||||
assertTrue(migration.contains("2026-08-17 / 015"));
|
||||
assertTrue(migration.contains("add column inbound_order_id bigint null"));
|
||||
assertTrue(migration.contains("quantity columns remain untouched"));
|
||||
assertTrue(migration.contains("2026-08-17 / 018"));
|
||||
assertTrue(migration.contains("alter table ll_order add column picked_package_count"));
|
||||
assertTrue(migration.contains("alter table ll_order add column shipped_package_count"));
|
||||
assertTrue(migration.contains("alter table ll_inventory_balance add column inbound_order_no"));
|
||||
assertTrue(migration.contains("update ll_inventory_balance"));
|
||||
assertTrue(migration.contains("where inbound_order_id is null or inbound_code is null or inbound_code = ''"));
|
||||
assertTrue(migration.contains("alter table ll_inventory_ledger add column inbound_order_id"));
|
||||
assertTrue(migration.contains("update ll_inventory_ledger"));
|
||||
assertTrue(migration.contains("alter table ll_dispatch_order add column partial_completion_reason"));
|
||||
assertFalse(migration.contains("drop column sku_code"));
|
||||
assertFalse(migration.contains("drop column product_quantity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void entitiesExposeInboundOrderPackageDimensionAndLegacyQuantityPlaceholders() throws NoSuchFieldException {
|
||||
assertNotNull(InventoryBalance.class.getDeclaredField("inboundOrderId"));
|
||||
assertNotNull(InventoryBalance.class.getDeclaredField("inboundCode"));
|
||||
assertNotNull(InventoryLedger.class.getDeclaredField("inboundOrderId"));
|
||||
assertThrowsNoField(InventoryBalance.class, "skuCode");
|
||||
assertThrowsNoField(InventoryBalance.class, "availableQuantity");
|
||||
assertThrowsNoField(InventoryPosition.class, "skuCode");
|
||||
assertThrowsNoField(InventoryPosition.class, "productQuantity");
|
||||
assertNotNull(InventoryLedger.class.getDeclaredField("skuCode"));
|
||||
assertNotNull(InventoryLedger.class.getDeclaredField("cartonMark"));
|
||||
assertNotNull(InventoryLedger.class.getDeclaredField("beforeQuantity"));
|
||||
assertNotNull(InventoryLedger.class.getDeclaredField("deltaQuantity"));
|
||||
assertNotNull(InventoryLedger.class.getDeclaredField("afterQuantity"));
|
||||
}
|
||||
|
||||
private static void assertThrowsNoField(Class<?> type, String fieldName) {
|
||||
assertTrue(java.util.Arrays.stream(type.getDeclaredFields()).noneMatch(field -> field.getName().equals(fieldName)),
|
||||
() -> type.getSimpleName() + " must not expose " + fieldName);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
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", 101L, "IN-CODE-1")).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 stockInAllowsPalletPositionWithZeroOptionalPackages() {
|
||||
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1")).thenReturn(null);
|
||||
|
||||
var result = service.stockIn("100", "7", stockIn("IN-1", "stock-in-zero", 0, 0));
|
||||
|
||||
assertEquals(0, result.availablePackages());
|
||||
verify(positions).insert(any(InventoryPosition.class));
|
||||
verify(ledgers).insert(any(InventoryLedger.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, "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", 101L, "IN-CODE-1")).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);
|
||||
InventoryPosition position = position(201L, 200L, 0);
|
||||
position.setPickedPackages(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, "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", 101L, inbound, "IN-CODE-1", 300L, "PLT-1", null, null, null,
|
||||
packages, "ORD-1", "EVT-1", key, null);
|
||||
}
|
||||
|
||||
private InventoryBalance balance(Long id, long available) {
|
||||
InventoryBalance value = new InventoryBalance();
|
||||
value.setId(id); value.setCustomerId("100"); value.setOverseasWarehouseId("WH-1");
|
||||
value.setDestinationWarehouseCode("ONT8"); value.setInboundOrderId(101L);
|
||||
value.setInboundOrderNo("IN-1"); value.setInboundCode("IN-CODE-1");
|
||||
value.setAvailablePackages(available);
|
||||
value.setHoldPackages(0L); value.setAllocatedPackages(0L); value.setPickedPackages(0L);
|
||||
value.setVersion(0L);
|
||||
return value;
|
||||
}
|
||||
|
||||
private InventoryPosition position(Long id, Long balanceId, long available) {
|
||||
InventoryPosition value = new InventoryPosition();
|
||||
value.setId(id); value.setCustomerId("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.setVersion(0L);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package com.baiyue.lastleg.inventory.command.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
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 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.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
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.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InventoryCommandControllerTest {
|
||||
@Mock InventoryCommandService service;
|
||||
private InventoryCommandController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new InventoryCommandController();
|
||||
ReflectionTestUtils.setField(controller, "service", service);
|
||||
}
|
||||
|
||||
@Test
|
||||
void inventoryMutationRequiresFormPermission() {
|
||||
var request = move();
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> controller.hold(user(Set.of()), request));
|
||||
assertEquals(403, exception.getStatusCode().value());
|
||||
verify(service, never()).hold(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void permittedBackOfficeUserCanRunInventoryFormCommand() {
|
||||
var request = move();
|
||||
when(service.hold("100", "7", request)).thenReturn(response());
|
||||
controller.hold(user(Set.of(InventoryCommandController.MANAGE_PERMISSION)), request);
|
||||
verify(service).hold("100", "7", request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void physicalAdjustmentRequiresDedicatedPermission() {
|
||||
var request = new InventoryCommandRequests.Adjustment("100", 1L, 2, "COUNT", "ORD-1", "adj-1", null);
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> controller.adjust(user(Set.of(InventoryCommandController.MANAGE_PERMISSION)), request));
|
||||
assertEquals(403, exception.getStatusCode().value());
|
||||
verify(service, never()).adjust(any(), any(), any());
|
||||
}
|
||||
|
||||
private InventoryCommandRequests.Move move() {
|
||||
return new InventoryCommandRequests.Move("100", 1L, 2, "ORD-1", null, null, "hold-1", null);
|
||||
}
|
||||
|
||||
private AuthenticatedUser user(Set<String> permissions) {
|
||||
return new AuthenticatedUser(7L, 100L, 100L, 8L, "operator", "MERCHANT", "WEB", permissions);
|
||||
}
|
||||
|
||||
private InventoryCommandResponse response() {
|
||||
return new InventoryCommandResponse(1L, "HOLD", 2L, 3L, 0, 2, 0, 0, false, false);
|
||||
}
|
||||
}
|
||||
+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, 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 customerId, String appCode) {
|
||||
return new AuthenticatedUser(1L, customerId, customerId, 1L, "tester", appCode, "WEB", Set.of());
|
||||
}
|
||||
|
||||
private InventoryQueryController controller() {
|
||||
InventoryQueryController controller = new InventoryQueryController();
|
||||
ReflectionTestUtils.setField(controller, "service", service);
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package com.baiyue.lastleg.inventory.query.service;
|
||||
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
|
||||
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
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.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
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.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);
|
||||
when(mapper.aggregate("100", query)).thenReturn(aggregate);
|
||||
when(mapper.countPallets("100", query)).thenReturn(3L);
|
||||
|
||||
InventoryDashboardResponse result = service.dashboard("100", query);
|
||||
|
||||
assertEquals(2, result.inboundOrderCount());
|
||||
assertEquals(3, result.palletCount());
|
||||
assertEquals(12, result.totalPackages());
|
||||
}
|
||||
|
||||
@Test
|
||||
void balancePageIsBoundToRequestedTenant() {
|
||||
InventoryQueryRequests.BalanceQuery query = balanceQuery(null);
|
||||
InventoryBalance balance = new InventoryBalance();
|
||||
balance.setId(1L);
|
||||
balance.setCustomerId("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);
|
||||
assertThrows(ResponseStatusException.class, () -> service.positions("100", 99L, query));
|
||||
verify(mapper, never()).positions(eq("100"), eq(99L), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void positionDetailCarriesInboundOrderAndInboundCodeFromOwnedBalance() {
|
||||
InventoryQueryRequests.PositionQuery query = new InventoryQueryRequests.PositionQuery(null, null, null);
|
||||
InventoryBalance balance = new InventoryBalance();
|
||||
balance.setId(99L);
|
||||
balance.setInboundOrderId(88L);
|
||||
balance.setInboundOrderNo("IBP-20260817-001");
|
||||
balance.setInboundCode("RK-001");
|
||||
InventoryPosition position = new InventoryPosition();
|
||||
position.setBalanceId(99L);
|
||||
position.setAvailablePackages(7L);
|
||||
when(mapper.ownedBalance("100", 99L)).thenReturn(balance);
|
||||
when(mapper.positions("100", 99L, query)).thenReturn(List.of(position));
|
||||
|
||||
var result = service.positions("100", 99L, query).get(0);
|
||||
|
||||
assertEquals(88L, result.inboundOrderId());
|
||||
assertEquals("IBP-20260817-001", result.inboundOrderNo());
|
||||
assertEquals("RK-001", result.inboundCode());
|
||||
assertEquals(7L, result.totalPackages());
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void balanceExportUsesInboundOrderPackageColumnsOnly() {
|
||||
InventoryQueryRequests.BalanceQuery query = balanceQuery(null);
|
||||
when(mapper.balances("100", query, 0, 10_000)).thenReturn(List.of());
|
||||
|
||||
String csv = new String(service.exportBalances("100", query), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(csv.contains("inboundOrderNo,inboundCode,totalPackages"));
|
||||
assertFalse(csv.contains("skuCode"));
|
||||
assertFalse(csv.contains("Quantity"));
|
||||
}
|
||||
|
||||
private static InventoryQueryRequests.BalanceQuery balanceQuery(String status) {
|
||||
return new InventoryQueryRequests.BalanceQuery(1, 20, null, null, null, null, null, null, null, status);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user