diff --git a/file/src/main/java/com/baiyue/file/service/ExportSchemaContracts.java b/file/src/main/java/com/baiyue/file/service/ExportSchemaContracts.java index d3e76f9ea..629d70a0c 100644 --- a/file/src/main/java/com/baiyue/file/service/ExportSchemaContracts.java +++ b/file/src/main/java/com/baiyue/file/service/ExportSchemaContracts.java @@ -28,7 +28,7 @@ final class ExportSchemaContracts { column("mainStatus", "订单主状态", ExportColumnType.STRING), column("customerName", "客户", ExportColumnType.STRING), column("channelName", "渠道", ExportColumnType.STRING), - column("channelChanged", "曾变更渠道", ExportColumnType.BOOLEAN), + column("channelChanged", "曾变更渠道", ExportColumnType.STRING), column("receivingWarehouseName", "定位仓库", ExportColumnType.STRING), column("destinationWarehouseName", "交货仓", ExportColumnType.STRING), column("expectedShipDate", "预计出运日期", ExportColumnType.DATE), @@ -44,7 +44,7 @@ final class ExportSchemaContracts { column("skuNos", "SKU", ExportColumnType.STRING), column("taxMode", "报关方式", ExportColumnType.STRING), column("containerMode", "装柜方式", ExportColumnType.STRING), - column("customsInspection", "是否商检", ExportColumnType.BOOLEAN), + column("customsInspection", "是否商检", ExportColumnType.STRING), column("containerNo", "集装箱号", ExportColumnType.STRING), column("cabinetNo", "柜次号", ExportColumnType.STRING), column("vesselName", "船名", ExportColumnType.STRING), diff --git a/file/src/main/resources/openapi/domestic-order-export-v1.yaml b/file/src/main/resources/openapi/domestic-order-export-v1.yaml index e2b788aad..befe98689 100644 --- a/file/src/main/resources/openapi/domestic-order-export-v1.yaml +++ b/file/src/main/resources/openapi/domestic-order-export-v1.yaml @@ -114,7 +114,7 @@ components: mainStatus: {type: string, title: 订单主状态} customerName: {type: string, title: 客户} channelName: {type: string, title: 渠道} - channelChanged: {type: boolean, title: 曾变更渠道} + channelChanged: {type: string, title: 曾变更渠道, description: 是或否} receivingWarehouseName: {type: string, title: 定位仓库} destinationWarehouseName: {type: string, title: 交货仓} expectedShipDate: {type: string, format: date, title: 预计出运日期} @@ -130,7 +130,7 @@ components: skuNos: {type: string, title: SKU} taxMode: {type: string, title: 报关方式} containerMode: {type: string, title: 装柜方式} - customsInspection: {type: boolean, title: 是否商检} + customsInspection: {type: string, title: 是否商检, description: 是或否} containerNo: {type: string, title: 集装箱号} cabinetNo: {type: string, title: 柜次号} vesselName: {type: string, title: 船名} diff --git a/file/src/test/java/com/baiyue/file/config/DomesticWarehouseOrderExportContractTest.java b/file/src/test/java/com/baiyue/file/config/DomesticWarehouseOrderExportContractTest.java index fbe7daa45..97bcb5f18 100644 --- a/file/src/test/java/com/baiyue/file/config/DomesticWarehouseOrderExportContractTest.java +++ b/file/src/test/java/com/baiyue/file/config/DomesticWarehouseOrderExportContractTest.java @@ -57,6 +57,8 @@ class DomesticWarehouseOrderExportContractTest { assertThat(columns).containsKeys("expectedVolumeCbm", "expectedArrivalAt") .doesNotContainKeys("orderId", "tenantId", "customerId", "inboundId", "receivingWarehouseId", "destinationWarehouseId"); + assertThat(map(columns.get("channelChanged")).get("type")).isEqualTo("string"); + assertThat(map(columns.get("customsInspection")).get("type")).isEqualTo("string"); assertThat(source).contains("platform:order:export", "selectedOrderNos 非空时优先", "no-store"); assertThat(paths).containsKey("/api/platform/v1/files/export-tasks/{taskId}/retry"); assertThat(map(map(schemas.get("ExportTaskRetryState")).get("properties"))) diff --git a/file/src/test/java/com/baiyue/file/service/GenericXlsxExportRunnerTest.java b/file/src/test/java/com/baiyue/file/service/GenericXlsxExportRunnerTest.java index d983ad4ba..50efcf033 100644 --- a/file/src/test/java/com/baiyue/file/service/GenericXlsxExportRunnerTest.java +++ b/file/src/test/java/com/baiyue/file/service/GenericXlsxExportRunnerTest.java @@ -157,6 +157,9 @@ class GenericXlsxExportRunnerTest { Map row = new java.util.LinkedHashMap<>(); row.put("inboundCode", "IN-1"); row.put("orderNo", "ORD-1"); row.put("expectedVolumeCbm", "1.25"); row.put("expectedArrivalAt", "2026-09-03T12:30:00"); + row.put("mainStatus", "待审核"); row.put("channelChanged", "否"); + row.put("feeReviewStatus", "未提交"); row.put("taxMode", "买单报关"); + row.put("containerMode", "拼箱"); row.put("customsInspection", "否"); var batch = new ExportBatchResponse(1, "sha256:domestic-orders", "snapshot-orders", columns, List.of(row), null, false, 1L); when(provider.batch(anyString(), eq(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST), any(), any())) @@ -173,6 +176,12 @@ class GenericXlsxExportRunnerTest { assertEquals("预计到货时间", sheet.getRow(0).getCell(9).getStringCellValue()); assertEquals("预计体积(m³)", sheet.getRow(0).getCell(10).getStringCellValue()); assertEquals(1.25d, sheet.getRow(1).getCell(10).getNumericCellValue()); + assertEquals("待审核", sheet.getRow(1).getCell(2).getStringCellValue()); + assertEquals("否", sheet.getRow(1).getCell(5).getStringCellValue()); + assertEquals("未提交", sheet.getRow(1).getCell(17).getStringCellValue()); + assertEquals("买单报关", sheet.getRow(1).getCell(19).getStringCellValue()); + assertEquals("拼箱", sheet.getRow(1).getCell(20).getStringCellValue()); + assertEquals("否", sheet.getRow(1).getCell(21).getStringCellValue()); } ArgumentCaptor request = ArgumentCaptor.forClass(ExportBatchRequest.class); verify(provider).batch(anyString(), eq(ExportSchemaContracts.DOMESTIC_WAREHOUSE_ORDER_LIST), diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java index deda6cb94..363b15edb 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/InboundMeasurementResponses.java @@ -23,6 +23,8 @@ public final class InboundMeasurementResponses { Integer expectedPalletCount, String processStatus, String measureReviewStatus, + @Schema(description = "当前最新费用版本是否已由客户确认;确认后测量数据不可再修改") + boolean customerFeeConfirmed, @Schema(description = "测量完成后的推荐托盘类型,仅为建议,不代表已分配托盘", allowableValues = "STANDARD", example = "STANDARD") String recommendedPalletType, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java index 1deb3c156..d82ca93ec 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/InboundMeasurementService.java @@ -109,7 +109,7 @@ public class InboundMeasurementService { MeasureReviewRecordEntity review = repository.findLatestReview(tenantId, inbound.getId()); LatestDeclaration latestDeclaration = loadLatestDeclaration(inbound); return toDetail(inbound, measurement, items, weights, allocationSlots, allocationsByWeight, - review, latestDeclaration); + review, latestDeclaration, customerFeeConfirmed(tenantId, inbound.getId())); } public PageResult weightPalletOptions( @@ -236,10 +236,9 @@ public class InboundMeasurementService { InboundOrderEntity inbound, String subject) { var currentFee = pricingRepository.findLatestFeeForUpdate(tenantId, inboundId); boolean customerConfirmed = currentFee != null && currentFee.getCustomerConfirmedAt() != null; - boolean loadingCompleted = Set.of("LOADED", "DELIVERED").contains(inbound.getProcessStatus()); - if (customerConfirmed && loadingCompleted) { + if (customerConfirmed) { throw new BizException(ResultCode.CONFLICT, - "客户已确认且该票已装柜完成,不能修改" + subject); + "客户已确认费用,不能修改" + subject); } } @@ -569,12 +568,14 @@ public class InboundMeasurementService { List allocationSlots, Map allocationsByWeight, MeasureReviewRecordEntity review, - LatestDeclaration latestDeclaration) { + LatestDeclaration latestDeclaration, + boolean customerFeeConfirmed) { InboundMeasurementResponses.DeclarationSnapshot declaration = latestDeclaration.snapshot(); return new InboundMeasurementResponses.Detail( inbound.getId(), inbound.getInboundCode(), customerDisplayName(inbound), inbound.getChannelCode(), inbound.getChannelName(), inbound.getFulfillmentType(), inbound.getPalletCount(), inbound.getProcessStatus(), inbound.getMeasureReviewStatus(), + customerFeeConfirmed, RECOMMENDED_PALLET_TYPE, review == null ? null : review.getReviewReason(), review == null ? null : review.getReviewerId(), review == null ? null : review.getReviewerName(), measurement == null ? null : measurement.getId(), @@ -603,6 +604,11 @@ public class InboundMeasurementService { toPalletLocationGuide(allocationSlots)); } + private boolean customerFeeConfirmed(long tenantId, long inboundId) { + var latestFee = pricingRepository.findLatestFee(tenantId, inboundId); + return latestFee != null && latestFee.getCustomerConfirmedAt() != null; + } + private static InboundMeasurementResponses.PalletLocationGuide toPalletLocationGuide( List slots) { if (slots.isEmpty()) return null; diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index 166b0edf4..8b469f10e 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -1018,8 +1018,7 @@ paths: summary: 更新箱规计算规则 description: >- 独立更新当前测量批次全部明细的箱规计算规则并重算体积,不覆盖整批测量数据, - 不改变测量提交或审核状态。客户未确认或该票尚未装柜完成时均可修改; - 仅当客户已确认费用且该票已装柜完成时冻结。 + 不改变测量提交或审核状态。客户确认费用后立即冻结,不得再修改。 parameters: - {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} - {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} @@ -1036,8 +1035,7 @@ paths: summary: 更新仓库外箱长宽高 description: >- 独立更新当前测量批次指定明细的仓库外箱尺寸并重算体积,不覆盖整批测量数据, - 不改变测量提交或审核状态。客户未确认或该票尚未装柜完成时均可修改; - 仅当客户已确认费用且该票已装柜完成时冻结。 + 不改变测量提交或审核状态。客户确认费用后立即冻结,不得再修改。 parameters: - {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} - {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}} @@ -1905,7 +1903,7 @@ components: type: object description: >- 入库测量权威详情。尺寸证据天然归属 items,称重证据天然归属 weightResults,响应不提供统一 images。 - required: [inboundId, inboundVersion, items, weightResults] + required: [inboundId, inboundVersion, customerFeeConfirmed, items, weightResults] properties: inboundId: {type: string, pattern: '^[1-9][0-9]*$'} inboundCode: {type: string, nullable: true, description: 当前测量详情唯一入仓码,UI业务主展示字段} @@ -1914,6 +1912,7 @@ components: channelName: {type: string, nullable: true} processStatus: {type: string, nullable: true} measureReviewStatus: {type: string, nullable: true} + customerFeeConfirmed: {type: boolean, description: 当前最新费用版本已由客户确认,确认后测量数据不可再修改} recommendedPalletType: {type: string, nullable: true} reviewReason: {type: string, nullable: true} reviewerId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java index 5daf6a6b5..1d47e2c4a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java @@ -124,8 +124,8 @@ class InboundMeasurementServiceTest { } @Test - void locksCalculationStrategyOnlyAfterCustomerConfirmationAndLoadingCompletion() { - InboundOrderEntity inbound = inbound("LOADED"); + void locksCalculationStrategyAsSoonAsCustomerConfirmsFee() { + InboundOrderEntity inbound = inbound("WAIT_SPLIT"); InboundFeeEntity fee = new InboundFeeEntity(); fee.setCustomerConfirmedAt(LocalDateTime.now()); when(repository.lockInbound(1L, 10L)).thenReturn(inbound); @@ -134,7 +134,7 @@ class InboundMeasurementServiceTest { assertThatThrownBy(() -> service.updateCalculationStrategy(1L, 10L, new InboundMeasurementRequests.UpdateCalculationStrategyRequest(30L, 5, "ACTUAL", null))) .isInstanceOf(BizException.class) - .hasMessageContaining("客户已确认且该票已装柜完成"); + .hasMessageContaining("客户已确认费用"); verify(repository, never()).lockMeasurement(anyLong(), anyLong(), anyLong()); } @@ -245,6 +245,23 @@ class InboundMeasurementServiceTest { verify(repository, never()).updateInbound(any()); } + @Test + void detailReportsCustomerFeeConfirmationForTheMeasurementLock() { + InboundOrderEntity inbound = inbound("WAIT_SPLIT"); + InboundMeasurementEntity measurement = new InboundMeasurementEntity(); + measurement.setId(30L); + InboundFeeEntity fee = new InboundFeeEntity(); + fee.setCustomerConfirmedAt(LocalDateTime.now()); + when(repository.findInbound(1L, 10L)).thenReturn(inbound); + when(repository.findLatestMeasurement(1L, 10L)).thenReturn(measurement); + when(repository.listWeights(1L, 10L)).thenReturn(List.of()); + when(pricingRepository.findLatestFee(1L, 10L)).thenReturn(fee); + + InboundMeasurementResponses.Detail result = service.detail(1L, 10L); + + assertThat(result.customerFeeConfirmed()).isTrue(); + } + @Test void detailReportsOrderContractNotReadyInsteadOfExposingDownstreamBadRequest() { InboundOrderEntity inbound = inbound("MEASURE_PENDING"); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java index eb2564c80..8056cd113 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java @@ -51,6 +51,7 @@ public final class DispatchRequests { public record Update(@Size(max = 64) String vehiclePlate, @Size(max = 128) String driverName, @Size(max = 64) String driverPhone, @Size(max = 255) String carrierName, + LocalDateTime departAt, @Size(max = 64) String fbaAppointmentNo, @Size(max = 32) String fbaAppointmentStatus, @Size(max = 32) String vehicleAppointmentStatus, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java index 6a097c582..f504f93d4 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java @@ -29,6 +29,14 @@ public interface DispatchCandidateMapper { "MIN(l.order_id) AS orderId," + "MIN(ip.customer_id) AS customerId,MIN(o.customer_name) AS customerName," + "MIN(ip.plan_no) AS planNo,MIN(o.order_type) AS orderType,MIN(o.volume_weight_kg) AS volumeWeightKg," + + "COALESCE(NULLIF(SUM(CASE WHEN COALESCE(l.volume_cbm,0)>0 THEN " + + "COALESCE((SELECT SUM(d.gross_weight_kg) FROM ll_order_detail d WHERE d.customer_id=ip.customer_id AND d.order_id=l.order_id)," + + "(SELECT inbound.gross_weight_kg FROM ll_inbound_order inbound WHERE inbound.id=l.order_id AND inbound.customer_id=ip.customer_id LIMIT 1),0) " + + "* COALESCE(a.allocated_volume_cbm,0) / l.volume_cbm ELSE " + + "COALESCE((SELECT SUM(d.gross_weight_kg) FROM ll_order_detail d WHERE d.customer_id=ip.customer_id AND d.order_id=l.order_id)," + + "(SELECT inbound.gross_weight_kg FROM ll_inbound_order inbound WHERE inbound.id=l.order_id AND inbound.customer_id=ip.customer_id LIMIT 1),0) " + + "/ NULLIF((SELECT COUNT(*) FROM ll_inbound_plan_pallet_allocation a2 WHERE a2.line_id=a.line_id),0) END),0)," + + "MIN(o.volume_weight_kg)) AS grossWeightKg," + "MIN(ip.container_no) AS containerNo," + "(SELECT MAX(fa.appointment_at) FROM ll_inbound_fba_appointment_item fi " + "JOIN ll_inbound_fba_appointment fa ON fa.id=fi.appointment_id WHERE fi.line_id IN " + @@ -38,7 +46,13 @@ public interface DispatchCandidateMapper { "(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id)) AS expectedLoadingAt," + "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id) " + "FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id " + - "AND pos.available_packages > 0) ELSE CAST(ip.overseas_warehouse_id AS CHAR) END AS overseasWarehouseId," + + "AND pos.available_packages > 0) ELSE COALESCE(NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')," + + "(SELECT NULLIF(TRIM(source_order.overseas_warehouse_id),'') FROM ll_order source_order " + + "WHERE source_order.id=ip.source_order_id AND source_order.customer_id=ip.customer_id LIMIT 1)) END AS overseasWarehouseId," + + "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_code) " + + "FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " + + "WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " + + "ELSE MIN(o.overseas_warehouse_code) END AS overseasWarehouseCode," + "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_name) " + "FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " + "WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " + @@ -115,6 +129,14 @@ public interface DispatchCandidateMapper { "MIN(l.order_id) AS orderId," + "MIN(ip.customer_id) AS customerId,MIN(o.customer_name) AS customerName," + "MIN(ip.plan_no) AS planNo,MIN(o.order_type) AS orderType,MIN(o.volume_weight_kg) AS volumeWeightKg," + + "COALESCE(NULLIF(SUM(CASE WHEN COALESCE(l.volume_cbm,0)>0 THEN " + + "COALESCE((SELECT SUM(d.gross_weight_kg) FROM ll_order_detail d WHERE d.customer_id=ip.customer_id AND d.order_id=l.order_id)," + + "(SELECT inbound.gross_weight_kg FROM ll_inbound_order inbound WHERE inbound.id=l.order_id AND inbound.customer_id=ip.customer_id LIMIT 1),0) " + + "* COALESCE(a.allocated_volume_cbm,0) / l.volume_cbm ELSE " + + "COALESCE((SELECT SUM(d.gross_weight_kg) FROM ll_order_detail d WHERE d.customer_id=ip.customer_id AND d.order_id=l.order_id)," + + "(SELECT inbound.gross_weight_kg FROM ll_inbound_order inbound WHERE inbound.id=l.order_id AND inbound.customer_id=ip.customer_id LIMIT 1),0) " + + "/ NULLIF((SELECT COUNT(*) FROM ll_inbound_plan_pallet_allocation a2 WHERE a2.line_id=a.line_id),0) END),0)," + + "MIN(o.volume_weight_kg)) AS grossWeightKg," + "MIN(ip.container_no) AS containerNo," + "(SELECT MAX(fa.appointment_at) FROM ll_inbound_fba_appointment_item fi " + "JOIN ll_inbound_fba_appointment fa ON fa.id=fi.appointment_id WHERE fi.line_id IN " + @@ -124,7 +146,13 @@ public interface DispatchCandidateMapper { "(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id)) AS expectedLoadingAt," + "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id) " + "FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id " + - "AND pos.available_packages > 0) ELSE CAST(ip.overseas_warehouse_id AS CHAR) END AS overseasWarehouseId," + + "AND pos.available_packages > 0) ELSE COALESCE(NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')," + + "(SELECT NULLIF(TRIM(source_order.overseas_warehouse_id),'') FROM ll_order source_order " + + "WHERE source_order.id=ip.source_order_id AND source_order.customer_id=ip.customer_id LIMIT 1)) END AS overseasWarehouseId," + + "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_code) " + + "FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " + + "WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " + + "ELSE MIN(o.overseas_warehouse_code) END AS overseasWarehouseCode," + "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_name) " + "FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " + "WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " + @@ -185,8 +213,8 @@ public interface DispatchCandidateMapper { @Select("") List balances(@Param("customerId") String customerId, @Param("q") InventoryQueryRequests.BalanceQuery query, @Param("offset") long offset, @Param("limit") long limit); - @Select("") + @Select("") long countBalances(@Param("customerId") String customerId, @Param("q") InventoryQueryRequests.BalanceQuery query); + @Select("") + List sourceBalances(@Param("customerId") String customerId, + @Param("q") InventoryQueryRequests.BalanceQuery query, + @Param("offset") long offset, + @Param("limit") long limit); + @Select("