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)); }