diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/mapper/InventoryQueryMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/mapper/InventoryQueryMapper.java
index c4e9d002e..316eb2b8d 100644
--- a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/mapper/InventoryQueryMapper.java
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/mapper/InventoryQueryMapper.java
@@ -25,7 +25,7 @@ public interface InventoryQueryMapper {
" AND EXISTS (SELECT 1 FROM ll_inventory_position p WHERE p.balance_id=ll_inventory_balance.id AND p.customer_id=ll_inventory_balance.customer_id AND p.batch_no LIKE CONCAT('%',#{q.batchNo},'%'))" +
" AND EXISTS (SELECT 1 FROM ll_inventory_position p WHERE p.balance_id=ll_inventory_balance.id AND p.customer_id=ll_inventory_balance.customer_id AND p.warehouse_location_code LIKE CONCAT('%',#{q.warehouseLocationCode},'%'))" +
"" +
- " AND available_packages > 0" +
+ " AND (available_packages+allocated_packages+picked_packages) > 0" +
" AND hold_packages > 0" +
" AND allocated_packages > 0" +
" AND picked_packages > 0" +
@@ -48,7 +48,7 @@ public interface InventoryQueryMapper {
" AND EXISTS (SELECT 1 FROM ll_inventory_position p2 WHERE p2.balance_id=b.id AND p2.customer_id=b.customer_id AND p2.batch_no LIKE CONCAT('%',#{q.batchNo},'%'))" +
" AND EXISTS (SELECT 1 FROM ll_inventory_position p2 WHERE p2.balance_id=b.id AND p2.customer_id=b.customer_id AND p2.warehouse_location_code LIKE CONCAT('%',#{q.warehouseLocationCode},'%'))" +
"" +
- " AND b.available_packages > 0" +
+ " AND (b.available_packages+b.allocated_packages+b.picked_packages) > 0" +
" AND b.hold_packages > 0" +
" AND b.allocated_packages > 0" +
" AND b.picked_packages > 0" +
@@ -92,7 +92,7 @@ public interface InventoryQueryMapper {
" AND sku_code LIKE CONCAT('%',#{q.skuCode},'%')" +
" AND warehouse_location_code LIKE CONCAT('%',#{q.warehouseLocationCode},'%')" +
"" +
- " AND available_packages > 0" +
+ " AND (available_packages+allocated_packages+picked_packages) > 0" +
" AND hold_packages > 0" +
" AND allocated_packages > 0" +
" AND picked_packages > 0" +
diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryService.java
index 504d31f03..eb6bcab9a 100644
--- a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryService.java
+++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryService.java
@@ -41,7 +41,7 @@ public class InventoryQueryService {
long inTransitPackages = number(value.getInTransitPackages());
long receivingPackages = number(value.getReceivingPackages());
long procurementPackages = number(value.getProcurementPackages());
- long totalPackages = availablePackages + holdPackages + allocatedPackages + pickedPackages + receivingPackages;
+ long totalPackages = availablePackages + holdPackages + allocatedPackages + pickedPackages;
return new InventoryDashboardResponse(number(value.getDimensionCount()), mapper.countPallets(customerId, query),
totalPackages, Math.max(0, totalPackages - holdPackages), holdPackages, allocatedPackages,
pickedPackages, inTransitPackages,
@@ -117,7 +117,7 @@ public class InventoryQueryService {
long inTransitPackages = number(value.getInTransitPackages());
long receivingPackages = number(value.getReceivingPackages());
long procurementPackages = number(value.getProcurementPackages());
- long totalPackages = availablePackages + holdPackages + allocatedPackages + pickedPackages + receivingPackages;
+ long totalPackages = availablePackages + holdPackages + allocatedPackages + pickedPackages;
return new InventoryBalanceView(value.getId(), value.getCustomerId(), value.getCustomerName(), value.getOverseasWarehouseId(),
value.getOverseasWarehouseCode(), value.getOverseasWarehouseName(), value.getDestinationWarehouseId(),
value.getDestinationWarehouseCode(), value.getDestinationWarehouseName(), value.getInboundOrderId(),
@@ -133,7 +133,7 @@ public class InventoryQueryService {
long allocated = number(value.getAllocatedPackages());
long picked = number(value.getPickedPackages());
long receiving = number(value.getReceivingPackages());
- long total = available + hold + allocated + picked + receiving;
+ long total = available + hold + allocated + picked;
return new InventoryPositionView(value.getId(), value.getBalanceId(), value.getCustomerId(),
value.getOverseasWarehouseId(), value.getDestinationWarehouseCode(), balance.getInboundOrderId(),
balance.getInboundOrderNo(), balance.getInboundCode(),
diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryCommandServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryCommandServiceTest.java
index 02b4a1a05..1e34f715a 100644
--- a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryCommandServiceTest.java
+++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryCommandServiceTest.java
@@ -77,6 +77,34 @@ class InventoryCommandServiceTest {
verify(operations).updateById(any(InventoryOperation.class));
}
+ @Test
+ void registersInTransitWithoutCreatingAPalletPosition() {
+ when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1", "SKU-1")).thenReturn(null);
+
+ InventoryCommandResponse result = service.registerInTransit("100", "SYSTEM", inTransit("transit-1", 12));
+
+ assertEquals(0, result.availablePackages());
+ assertEquals(null, result.positionId());
+ ArgumentCaptor balance = ArgumentCaptor.forClass(InventoryBalance.class);
+ verify(balances).updateById(balance.capture());
+ assertEquals(12L, balance.getValue().getInTransitPackages());
+ verify(positions, never()).insert(any(InventoryPosition.class));
+ verify(ledgers).insert(any(InventoryLedger.class));
+ }
+
+ @Test
+ void stockInTransfersPackagesFromInTransitToAvailable() {
+ InventoryBalance balance = balance(200L, 0);
+ balance.setInTransitPackages(12L);
+ when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1", "SKU-1")).thenReturn(balance);
+
+ InventoryCommandResponse result = service.stockIn("100", "7", stockIn("IN-1", "stock-in-transit", 12, 24));
+
+ assertEquals(12, result.availablePackages());
+ assertEquals(0L, balance.getInTransitPackages());
+ assertEquals(12L, balance.getAvailablePackages());
+ }
+
@Test
void stockInAllowsPalletPositionWithZeroOptionalPackages() {
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1", "SKU-1")).thenReturn(null);
@@ -161,6 +189,12 @@ class InventoryCommandServiceTest {
packages, "ORD-1", "EVT-1", key, null);
}
+ private InventoryCommandRequests.InTransit inTransit(String key, long packages) {
+ return new InventoryCommandRequests.InTransit("Customer 100", "WH-1", "WH1", "Warehouse 1",
+ "DEST-1", "ONT8", "ONT8", 101L, "IN-1", "IN-CODE-1", "SKU-1", "Product 1", 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");
diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryServiceTest.java
index 73d7bb0b3..c6eee959a 100644
--- a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryServiceTest.java
+++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryServiceTest.java
@@ -50,6 +50,25 @@ class InventoryQueryServiceTest {
assertEquals(12, result.totalPackages());
}
+ @Test
+ void dashboardExcludesTransitAndProcurementFromOnHandAndDerivesAvailableFromHold() {
+ InventoryQueryRequests.BalanceQuery query = balanceQuery(null);
+ InventoryAggregateProjection aggregate = new InventoryAggregateProjection();
+ aggregate.setAvailablePackages(8L);
+ aggregate.setHoldPackages(2L);
+ aggregate.setAllocatedPackages(3L);
+ aggregate.setInTransitPackages(7L);
+ aggregate.setProcurementPackages(5L);
+ when(mapper.aggregate("100", query)).thenReturn(aggregate);
+
+ InventoryDashboardResponse result = service.dashboard("100", query);
+
+ assertEquals(13L, result.totalPackages());
+ assertEquals(11L, result.availablePackages());
+ assertEquals(7L, result.inTransitPackages());
+ assertEquals(5L, result.procurementPackages());
+ }
+
@Test
void balancePageIsBoundToRequestedTenant() {
InventoryQueryRequests.BalanceQuery query = balanceQuery(null);
@@ -65,6 +84,27 @@ class InventoryQueryServiceTest {
verify(mapper).balances("100", query, 0, 20);
}
+ @Test
+ void balanceViewUsesTheSameOnHandAndAvailableFormulaAsDashboard() {
+ InventoryQueryRequests.BalanceQuery query = balanceQuery(null);
+ InventoryBalance balance = new InventoryBalance();
+ balance.setId(1L);
+ balance.setCustomerId("100");
+ balance.setAvailablePackages(8L);
+ balance.setHoldPackages(2L);
+ balance.setAllocatedPackages(3L);
+ balance.setInTransitPackages(7L);
+ balance.setProcurementPackages(5L);
+ when(mapper.countBalances("100", query)).thenReturn(1L);
+ when(mapper.balances("100", query, 0, 20)).thenReturn(List.of(balance));
+
+ var result = service.balances("100", query).records().get(0);
+
+ assertEquals(13L, result.totalPackages());
+ assertEquals(11L, result.availablePackages());
+ assertEquals(7L, result.inTransitPackages());
+ }
+
@Test
void platformBalancePagePassesNoCustomerFilter() {
InventoryQueryRequests.BalanceQuery query = balanceQuery(null);
diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java
index c482c97b4..e32e7a663 100644
--- a/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java
+++ b/lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java
@@ -1,10 +1,10 @@
package com.baiyue.lastleg.service;
-import com.baiyue.common.event.*; import com.baiyue.lastleg.entity.*; import com.baiyue.lastleg.mapper.*; import com.baiyue.lastleg.dropship.dto.DropshipRequests; import com.baiyue.lastleg.dropship.service.DropshipOrderService; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper; import com.baiyue.lastleg.inboundplan.service.InboundPlanService; import com.fasterxml.jackson.databind.*; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; import org.springframework.test.util.ReflectionTestUtils; import java.math.BigDecimal; import java.time.Instant; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*;
+import com.baiyue.common.event.*; import com.baiyue.lastleg.entity.*; import com.baiyue.lastleg.mapper.*; import com.baiyue.lastleg.dropship.dto.DropshipRequests; import com.baiyue.lastleg.dropship.service.DropshipOrderService; import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper; import com.baiyue.lastleg.inboundplan.service.InboundPlanService; import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests; import com.baiyue.lastleg.inventory.command.service.InventoryCommandService; import com.fasterxml.jackson.databind.*; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; import org.springframework.test.util.ReflectionTestUtils; import java.math.BigDecimal; import java.time.Instant; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*;
@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
class LoadingCompletedConsumerTest {
- @Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InboundPlanService inboundPlans; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
- @BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inboundPlans",inboundPlans); ReflectionTestUtils.setField(service,"objectMapper",json); ReflectionTestUtils.setField(service,"orderNoGenerator",new LastlegOrderNoGenerator()); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); }
- @Test void createsOneOrderAndOneInboundPlanPerInbound(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); verify(orders,times(2)).insert(any(LastlegOrder.class)); verify(inboundPlans,times(2)).convert(eq("100"),anyLong()); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU1".equals(o.getContainerNo()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertEquals(new LastlegOrderNoGenerator().forContainerNo("MSCU1"),c.getAllValues().get(0).getOrderNo()); }
+ @Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InboundPlanService inboundPlans; @Mock InventoryCommandService inventory; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
+ @BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inboundPlans",inboundPlans); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"objectMapper",json); ReflectionTestUtils.setField(service,"orderNoGenerator",new LastlegOrderNoGenerator()); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); lenient().when(inboundPlans.convert(anyString(),anyLong())).thenReturn(new InboundPlanResponse(1L,"LIP-1","100",1L,"MSCU1","PENDING_ASSIGNMENT",1,1,BigDecimal.ONE,List.of(),List.of(),List.of())); }
+ @Test void createsOneOrderAndOneInboundPlanPerInbound(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); verify(orders,times(2)).insert(any(LastlegOrder.class)); verify(inboundPlans,times(2)).convert(eq("100"),anyLong()); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),any(InventoryCommandRequests.InTransit.class)); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU1".equals(o.getContainerNo()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertEquals(new LastlegOrderNoGenerator().forContainerNo("MSCU1"),c.getAllValues().get(0).getOrderNo()); }
@Test void repeatedEventReturnsExistingEffect(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); when(inbox.selectById(event.eventId())).thenReturn(row); when(orders.countContainer("100","MSCU1")).thenReturn(2L); assertEquals(2,service.consume(event)); verify(orders).countContainer("100","MSCU1"); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); }
@Test void sameEventWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(row); assertThrows(IllegalArgumentException.class,()->service.consume(event)); }
@Test void rejectsEventFromWrongRegion(){ EventEnvelope source=event(); EventEnvelope invalid=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),RegionCode.OVERSEAS,RegionCode.CN,source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),source.payloadHash()); assertThrows(IllegalArgumentException.class,()->service.consume(invalid)); }