From d320e78bbc04916cfbe97b0fe0e4b2007072015e Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 30 Sep 2026 13:00:40 +0800 Subject: [PATCH] fix(inventory): resolve returns from immutable shipment SKU facts --- .../inventory/command/dto/ShipmentSource.java | 4 ++ .../mapper/InventoryCommandMapper.java | 12 ++-- .../service/InventoryCommandService.java | 25 ++++++-- .../DispatchShipmentSourceQueryTest.java | 55 +++++++++++++--- .../command/InventoryCommandServiceTest.java | 64 +++++++++++++------ 5 files changed, 119 insertions(+), 41 deletions(-) create mode 100644 lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/ShipmentSource.java diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/ShipmentSource.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/ShipmentSource.java new file mode 100644 index 000000000..4409283a8 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/ShipmentSource.java @@ -0,0 +1,4 @@ +package com.baiyue.lastleg.inventory.command.dto; + +/** SKU and shipped quantity derived only from the selected immutable outbound shipment facts. */ +public record ShipmentSource(String skuCode, long shippedPackages) { } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java index dac3cac41..d52314fd8 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java @@ -1,6 +1,7 @@ package com.baiyue.lastleg.inventory.command.mapper; import com.baiyue.lastleg.inventory.entity.InventoryBalance; +import com.baiyue.lastleg.inventory.command.dto.ShipmentSource; import com.baiyue.lastleg.inventory.entity.InventoryPosition; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Delete; @@ -47,11 +48,11 @@ public interface InventoryCommandMapper { "AND NOT EXISTS (SELECT 1 FROM ll_inventory_position p WHERE p.balance_id=ll_inventory_balance.id)") int deleteEmptyBalance(@Param("customerId") String customerId, @Param("balanceId") Long balanceId); - @Select("SELECT COALESCE(-SUM(l.delta_packages),0) FROM ll_inventory_ledger l " + + @Select("SELECT l.sku_code AS skuCode, -SUM(l.delta_packages) AS shippedPackages FROM ll_inventory_ledger l " + "JOIN ll_outbound_order o ON o.customer_id=l.customer_id AND o.outbound_no=#{outboundNo} " + "AND o.overseas_warehouse_id=l.overseas_warehouse_id AND o.status='OUTBOUNDED' " + "WHERE l.customer_id=#{customerId} AND l.position_id=#{positionId} " + - "AND l.sku_code=#{skuCode} AND l.business_type='SHIP' AND l.delta_packages<0 " + + "AND l.business_type='SHIP' AND l.delta_packages<0 " + "AND ((o.outbound_type='DISPATCH_DELIVERY' AND l.business_order_no=o.dispatch_no " + "AND l.outbound_order_no=o.dispatch_no AND EXISTS (" + "SELECT 1 FROM ll_dispatch_pda_task t JOIN ll_dispatch_pda_pick p " + @@ -62,9 +63,10 @@ public interface InventoryCommandMapper { "WHERE t.customer_id=o.customer_id AND t.dispatch_order_id=o.dispatch_order_id " + "AND t.warehouse_id=o.overseas_warehouse_id AND p.inventory_position_id=l.position_id " + "AND p.packages=-l.delta_packages AND l.idempotency_key=CONCAT(op.idempotency_key,':',op.id))) " + - "OR (o.outbound_type<>'DISPATCH_DELIVERY' AND l.outbound_order_no=o.outbound_no))") - long shippedPackages(@Param("customerId") String customerId, @Param("positionId") Long positionId, - @Param("outboundNo") String outboundNo, @Param("skuCode") String skuCode); + "OR (o.outbound_type<>'DISPATCH_DELIVERY' AND l.outbound_order_no=o.outbound_no)) " + + "GROUP BY l.sku_code") + List shipmentSources(@Param("customerId") String customerId, @Param("positionId") Long positionId, + @Param("outboundNo") String outboundNo); @Select("SELECT COALESCE(SUM(delta_packages),0) FROM ll_inventory_ledger " + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java index d606f263b..8219e9a67 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java @@ -294,7 +294,7 @@ public class InventoryCommandService { || source.maxReturnPackages() <= 0) { throw badRequest("A valid returned inventory source is required"); } - String sourceSku = sku(source.skuCode()); + String sourceSku = trim(source.skuCode()); ReturnedInventoryRestoreRequest.SourceItem normalized = new ReturnedInventoryRestoreRequest.SourceItem( source.outboundPalletItemId(), source.inventoryPositionId(), sourceSku, source.maxReturnPackages()); if (sources.putIfAbsent(source.outboundPalletItemId(), normalized) != null) { @@ -351,6 +351,7 @@ public class InventoryCommandService { } Map lockedSources = new LinkedHashMap<>(); + Map resolvedSources = new LinkedHashMap<>(); Map balanceBefore = new LinkedHashMap<>(); for (ReturnedInventoryRestoreRequest.SourceItem source : sources.values()) { Locked locked = locked(customerId, source.inventoryPositionId()); @@ -361,22 +362,36 @@ public class InventoryCommandService { if (!warehouseId.equals(trim(balance.getOverseasWarehouseId()))) { throw conflict("Returned inventory source warehouse does not match"); } - long shipped = commands.shippedPackages(customerId, source.inventoryPositionId(), outboundNo, - source.skuCode()); - long restored = commands.restoredPackages(customerId, returnSourceIdentity(source), source.skuCode()); + var shipmentSources = commands.shipmentSources(customerId, source.inventoryPositionId(), outboundNo); + if (shipmentSources == null || shipmentSources.isEmpty()) { + throw conflict("Immutable outbound shipment source was not found"); + } + var matchingSources = source.skuCode() == null ? shipmentSources : shipmentSources.stream() + .filter(shipment -> source.skuCode().equals(shipment.skuCode())).toList(); + if (matchingSources.size() != 1) { + throw conflict(source.skuCode() == null ? "Immutable outbound shipment SKU source is ambiguous" + : "Declared return SKU does not match an immutable shipment source"); + } + var shipment = matchingSources.get(0); + String actualSku = required(shipment.skuCode(), "Immutable outbound shipment SKU is missing"); + var resolved = new ReturnedInventoryRestoreRequest.SourceItem(source.outboundPalletItemId(), + source.inventoryPositionId(), actualSku, source.maxReturnPackages()); + long shipped = shipment.shippedPackages(); + long restored = commands.restoredPackages(customerId, returnSourceIdentity(resolved), actualSku); long requested = requestedBySource.get(source.outboundPalletItemId()); if (shipped <= 0) throw conflict("Immutable outbound shipment source was not found"); if (restored + requested > shipped) { throw conflict("Returned packages exceed the remaining shipped packages"); } lockedSources.put(source.outboundPalletItemId(), locked); + resolvedSources.put(source.outboundPalletItemId(), resolved); balanceBefore.putIfAbsent(balance.getId(), value(balance.getAvailablePackages())); } long restoredPackages = 0; List ledgerIds = new java.util.ArrayList<>(); for (ReturnedInventoryRestoreRequest.Receipt receipt : request.receipts()) { - ReturnedInventoryRestoreRequest.SourceItem source = sources.get(receipt.outboundPalletItemId()); + ReturnedInventoryRestoreRequest.SourceItem source = resolvedSources.get(receipt.outboundPalletItemId()); Locked sourceLocked = lockedSources.get(receipt.outboundPalletItemId()); InventoryBalance balance = sourceLocked.balance(); WarehouseResourceGateway.WarehousePalletReference pallet = warehouseGateway.resolvePallet( diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/DispatchShipmentSourceQueryTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/DispatchShipmentSourceQueryTest.java index f79401126..50e59fb95 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/DispatchShipmentSourceQueryTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/DispatchShipmentSourceQueryTest.java @@ -1,6 +1,9 @@ package com.baiyue.lastleg.inventory.command; import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper; +import com.baiyue.lastleg.inventory.command.dto.ShipmentSource; +import java.util.List; +import java.util.ArrayList; import org.apache.ibatis.annotations.Select; import org.junit.jupiter.api.Test; @@ -28,18 +31,26 @@ class DispatchShipmentSourceQueryTest { try (var statement = connection.createStatement()) { statement.execute(sql); } } - private long shipped(Connection connection, String customerId, long positionId, String outboundNo, String sku) throws Exception { - String sql = String.join(" ", InventoryCommandMapper.class.getMethod("shippedPackages", - String.class, Long.class, String.class, String.class).getAnnotation(Select.class).value()) - .replace("#{outboundNo}", "?").replace("#{customerId}", "?") - .replace("#{positionId}", "?").replace("#{skuCode}", "?"); + private List sources(Connection connection, String customerId, long positionId, String outboundNo) throws Exception { + String sql = String.join(" ", InventoryCommandMapper.class.getMethod("shipmentSources", + String.class, Long.class, String.class).getAnnotation(Select.class).value()) + .replace("#{outboundNo}", "?").replace("#{customerId}", "?").replace("#{positionId}", "?"); try (var statement = connection.prepareStatement(sql)) { statement.setString(1, outboundNo); statement.setString(2, customerId); - statement.setLong(3, positionId); statement.setString(4, sku); - try (var result = statement.executeQuery()) { result.next(); return result.getLong(1); } + statement.setLong(3, positionId); + try (var result = statement.executeQuery()) { + List rows = new ArrayList<>(); + while (result.next()) rows.add(new ShipmentSource(result.getString("skuCode"), result.getLong("shippedPackages"))); + return rows; + } } } + private long shipped(Connection connection, String customerId, long positionId, String outboundNo, String sku) throws Exception { + return sources(connection, customerId, positionId, outboundNo).stream() + .filter(source -> sku.equals(source.skuCode())).mapToLong(ShipmentSource::shippedPackages).sum(); + } + private void scan(Connection connection) throws Exception { execute(connection, "INSERT INTO ll_inventory_operation VALUES (71,'100','SHIP','COMPLETED','DISPATCH:DS-1:31:pick-1:SHIP')"); execute(connection, "INSERT INTO ll_inventory_ledger VALUES (51,'100',41,'WH-1','SKU-1','SHIP',-8,'DS-1','DS-1','DISPATCH:DS-1:31:pick-1:SHIP:71')"); @@ -104,16 +115,40 @@ class DispatchShipmentSourceQueryTest { } @Test - void ordinaryFbaUnspecifiedSkuMatchesExactlyAndWrongSkuNeverFallsBack() throws Exception { + void resolvesOnlyActualImmutableSkuFactsAndGroupsQuantitiesForTheExactSource() throws Exception { try (Connection connection = database()) { scan(connection); transfer(connection); - execute(connection, "UPDATE ll_inventory_ledger SET sku_code='UNSPECIFIED'"); - assertEquals(8, shipped(connection, "100", 41, "OB-1", "UNSPECIFIED")); + assertEquals(List.of(new ShipmentSource("SKU-1", 8L)), sources(connection, "100", 41, "OB-1")); + execute(connection, "INSERT INTO ll_dispatch_pda_pick VALUES (22,'100',11,32,41,3,'pick-2')"); + execute(connection, "INSERT INTO ll_inventory_operation VALUES (72,'100','SHIP','COMPLETED','DISPATCH:DS-1:32:pick-2:SHIP')"); + execute(connection, "INSERT INTO ll_inventory_ledger VALUES (52,'100',41,'WH-1','SKU-1','SHIP',-3,'DS-1','DS-1','DISPATCH:DS-1:32:pick-2:SHIP:72')"); + assertEquals(List.of(new ShipmentSource("SKU-1", 11L)), sources(connection, "100", 41, "OB-1")); + execute(connection, "UPDATE ll_inventory_ledger SET sku_code='MIXED' WHERE id=52"); + assertEquals(2, sources(connection, "100", 41, "OB-1").size()); + assertEquals(8, shipped(connection, "100", 41, "OB-1", "SKU-1")); + assertEquals(3, shipped(connection, "100", 41, "OB-1", "MIXED")); assertEquals(0, shipped(connection, "100", 41, "OB-1", "")); + assertEquals(0, shipped(connection, "100", 41, "OB-1", "UNSPECIFIED")); assertEquals(0, shipped(connection, "100", 41, "OB-1", "WRONG-SKU")); } } + @Test + void productionMyBatisMapperMaterializesTheCanonicalShipmentSkuAndQuantity() throws Exception { + try (Connection connection = database()) { + scan(connection); transfer(connection); + var configuration = new org.apache.ibatis.session.Configuration(new org.apache.ibatis.mapping.Environment( + "source-sql", new org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory(), + new org.apache.ibatis.datasource.unpooled.UnpooledDataSource("org.h2.Driver", + connection.getMetaData().getURL(), "", ""))); + configuration.addMapper(InventoryCommandMapper.class); + try (var session = new org.apache.ibatis.session.SqlSessionFactoryBuilder().build(configuration).openSession(connection)) { + assertEquals(List.of(new ShipmentSource("SKU-1", 8L)), + session.getMapper(InventoryCommandMapper.class).shipmentSources("100", 41L, "OB-1")); + } + } + } + @Test void shipmentLedgerRequiresTheActualCompletedOperationIdentityRatherThanAPrefix() throws Exception { try (Connection connection = database()) { 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 4e0aa51c8..1e450ed8d 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 @@ -4,6 +4,7 @@ import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests; import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse; import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreRequest; import com.baiyue.lastleg.inventory.command.dto.ReturnedInventoryRestoreResponse; +import com.baiyue.lastleg.inventory.command.dto.ShipmentSource; import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper; import com.baiyue.lastleg.inventory.command.service.InventoryCommandService; import com.baiyue.lastleg.inventory.entity.InventoryBalance; @@ -20,8 +21,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.apache.ibatis.annotations.Select; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -361,7 +362,7 @@ class InventoryCommandServiceTest { InventoryPosition source = position(201L, 200L, 0); when(commands.lockPosition("100", 201L)).thenReturn(source); when(commands.lockBalance("100", 200L)).thenReturn(balance); - when(commands.shippedPackages("100", 201L, "OUT-1", "SKU-1")).thenReturn(8L); + when(commands.shipmentSources("100", 201L, "OUT-1")).thenReturn(List.of(new ShipmentSource("SKU-1", 8L))); when(commands.restoredPackages("100", "RETURN_SOURCE:301:201", "SKU-1")).thenReturn(1L); when(warehouseGateway.resolvePallet(400L)).thenReturn(new WarehouseResourceGateway.WarehousePalletReference( 400L, "PLT-RETURN", 900L, 500L, "IDLE", null, 0)); @@ -393,7 +394,7 @@ class InventoryCommandServiceTest { InventoryPosition source = position(201L, 200L, 0); when(commands.lockPosition("100", 201L)).thenReturn(source); when(commands.lockBalance("100", 200L)).thenReturn(balance); - when(commands.shippedPackages("100", 201L, "OUT-1", "SKU-1")).thenReturn(8L); + when(commands.shipmentSources("100", 201L, "OUT-1")).thenReturn(List.of(new ShipmentSource("SKU-1", 8L))); when(commands.restoredPackages("100", "RETURN_SOURCE:301:201", "SKU-1")).thenReturn(0L); when(warehouseGateway.resolvePallet(400L)).thenReturn(new WarehouseResourceGateway.WarehousePalletReference( 400L, "PLT-RETURN", 900L, 500L, "IDLE", null, 0)); @@ -413,15 +414,14 @@ class InventoryCommandServiceTest { } @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = {" "}) - void ordinaryFbaReturnUsesTheStockInSkuNormalizationAndReplaysOnlyOnce(String sourceSku) { + @MethodSource("missingSkuSources") + void missingFbaSummarySkuUsesUniqueImmutableShipmentSkuAndReplaysOnlyOnce(String sourceSku, String actualSku) { InventoryBalance balance = balance(200L, 0); balance.setOverseasWarehouseId("900"); - balance.setSkuCode("UNSPECIFIED"); + balance.setSkuCode("MIXED"); when(commands.lockPosition("100", 201L)).thenReturn(position(201L, 200L, 0)); when(commands.lockBalance("100", 200L)).thenReturn(balance); - when(commands.shippedPackages("100", 201L, "OUT-1", "UNSPECIFIED")).thenReturn(8L); + when(commands.shipmentSources("100", 201L, "OUT-1")).thenReturn(List.of(new ShipmentSource(actualSku, 8L))); when(warehouseGateway.resolvePallet(400L)).thenReturn(new WarehouseResourceGateway.WarehousePalletReference( 400L, "PLT-RETURN", 900L, 500L, "IDLE", null, 0)); when(warehouseGateway.resolveLocation(900L, 500L)).thenReturn( @@ -438,31 +438,51 @@ class InventoryCommandServiceTest { verify(operations).insert(operation.capture()); when(operations.findIdempotency("100", "return-1")).thenReturn(operation.getValue()); assertTrue(service.restoreReturnedInventory("100", "7", request).replayed()); - verify(commands).shippedPackages("100", 201L, "OUT-1", "UNSPECIFIED"); - verify(commands).restoredPackages("100", "RETURN_SOURCE:301:201", "UNSPECIFIED"); + verify(commands).shipmentSources("100", 201L, "OUT-1"); + verify(commands).restoredPackages("100", "RETURN_SOURCE:301:201", actualSku); ArgumentCaptor ledger = ArgumentCaptor.forClass(InventoryLedger.class); verify(ledgers, times(1)).insert(ledger.capture()); - assertEquals("UNSPECIFIED", ledger.getValue().getSkuCode()); + assertEquals(actualSku, ledger.getValue().getSkuCode()); + assertEquals(sourceSku, request.sources().get(0).skuCode()); + assertEquals("MIXED", balance.getSkuCode()); verify(positions, times(1)).insert(any(InventoryPosition.class)); verify(balances, times(1)).updateById(any(InventoryBalance.class)); } + private static java.util.stream.Stream missingSkuSources() { + return java.util.stream.Stream.of("SKU-1", "MIXED", "UNSPECIFIED").flatMap(actualSku -> + java.util.stream.Stream.of(null, "", " ").map(sourceSku -> Arguments.of(sourceSku, actualSku))); + } + @Test - void nonEmptyWrongReturnSkuIsRejectedWithoutSearchingAnotherSkuOrWritingInventory() { + void nonEmptyWrongReturnSkuIsRejectedWithoutSearchingAnotherSourceOrWritingInventory() { + rejectReturnSku("WRONG-SKU", List.of(new ShipmentSource("SKU-1", 8L))); + } + + @Test + void missingSummarySkuWithMultipleImmutableSkuGroupsIsRejectedWithoutInventoryMutation() { + rejectReturnSku(null, List.of(new ShipmentSource("SKU-1", 8L), new ShipmentSource("MIXED", 3L))); + } + + private void rejectReturnSku(String sourceSku, List shipments) { InventoryBalance balance = balance(200L, 0); balance.setOverseasWarehouseId("900"); - balance.setSkuCode("UNSPECIFIED"); + balance.setSkuCode("MIXED"); when(commands.lockPosition("100", 201L)).thenReturn(position(201L, 200L, 0)); when(commands.lockBalance("100", 200L)).thenReturn(balance); + when(commands.shipmentSources("100", 201L, "OUT-1")).thenReturn(shipments); ReturnedInventoryRestoreRequest base = returnRequest(1); ReturnedInventoryRestoreRequest request = new ReturnedInventoryRestoreRequest(base.exceptionNo(), base.outboundNo(), base.overseasWarehouseId(), base.inboundOrderId(), - List.of(new ReturnedInventoryRestoreRequest.SourceItem(301L, 201L, "WRONG-SKU", 5)), + List.of(new ReturnedInventoryRestoreRequest.SourceItem(301L, 201L, sourceSku, 5)), base.receipts(), base.idempotencyKey(), base.evidenceFileIds()); - assertThrows(ResponseStatusException.class, () -> service.restoreReturnedInventory("100", "7", request)); - verify(commands).shippedPackages("100", 201L, "OUT-1", "WRONG-SKU"); - verify(commands, never()).shippedPackages("100", 201L, "OUT-1", "UNSPECIFIED"); + ResponseStatusException failure = assertThrows(ResponseStatusException.class, + () -> service.restoreReturnedInventory("100", "7", request)); + assertEquals(sourceSku == null ? "Immutable outbound shipment SKU source is ambiguous" + : "Declared return SKU does not match an immutable shipment source", failure.getReason()); + verify(commands).shipmentSources("100", 201L, "OUT-1"); + verify(commands, never()).restoredPackages(any(), any(), any()); verify(positions, never()).insert(any(InventoryPosition.class)); verify(positions, never()).updateById(any(InventoryPosition.class)); verify(balances, never()).updateById(any(InventoryBalance.class)); @@ -475,7 +495,7 @@ class InventoryCommandServiceTest { balance.setOverseasWarehouseId("900"); when(commands.lockPosition("100", 201L)).thenReturn(position(201L, 200L, 0)); when(commands.lockBalance("100", 200L)).thenReturn(balance); - when(commands.shippedPackages("100", 201L, "OUT-1", "SKU-1")).thenReturn(4L); + when(commands.shipmentSources("100", 201L, "OUT-1")).thenReturn(List.of(new ShipmentSource("SKU-1", 4L))); when(commands.restoredPackages("100", "RETURN_SOURCE:301:201", "SKU-1")).thenReturn(2L); assertThrows(ResponseStatusException.class, @@ -489,14 +509,16 @@ class InventoryCommandServiceTest { @Test void returnRestoreSqlIsBoundToImmutableShipLedgerAndSourceIdentity() throws Exception { - String shippedSql = String.join(" ", InventoryCommandMapper.class.getMethod("shippedPackages", - String.class, Long.class, String.class, String.class).getAnnotation(Select.class).value()); + String shippedSql = String.join(" ", InventoryCommandMapper.class.getMethod("shipmentSources", + String.class, Long.class, String.class).getAnnotation(Select.class).value()); assertTrue(shippedSql.contains("position_id=#{positionId}")); assertTrue(shippedSql.contains("o.outbound_no=#{outboundNo}")); assertTrue(shippedSql.contains("p.inventory_position_id=l.position_id")); assertTrue(shippedSql.contains("p.idempotency_key,':SHIP'")); assertTrue(shippedSql.contains("l.idempotency_key=CONCAT(op.idempotency_key,':',op.id)")); assertTrue(shippedSql.contains("business_type='SHIP'")); + assertTrue(shippedSql.contains("GROUP BY l.sku_code")); + assertFalse(shippedSql.contains("#{skuCode}")); String restoredSql = String.join(" ", InventoryCommandMapper.class.getMethod("restoredPackages", String.class, String.class, String.class).getAnnotation(Select.class).value()); assertTrue(restoredSql.contains("source_event_id=#{sourceIdentity}"));