diff --git a/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceContainerSettlementController.java b/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceContainerSettlementController.java index 651b704da..0e10ae51a 100644 --- a/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceContainerSettlementController.java +++ b/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceContainerSettlementController.java @@ -27,7 +27,7 @@ public class PlatformFinanceContainerSettlementController { + "hasAuthority('platform:finance:settlement-ap:list')") @Operation(summary = "按客户和柜次批量查询已结算柜费及成本分摊") public ApiResponse details(@RequestParam long customerId, - @RequestParam long containerId) { - return FinanceResponses.success(service.details(customerId, containerId)); + @RequestParam long containerBookingId) { + return FinanceResponses.success(service.details(customerId, containerBookingId)); } } diff --git a/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java b/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java index 6cead789a..be239533d 100644 --- a/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java +++ b/finance/src/main/java/com/baiyue/finance/controller/PlatformFinanceController.java @@ -164,8 +164,8 @@ public class PlatformFinanceController { @BusinessAudit(event = "FINANCE_COST_CREATE", resourceType = "FINANCE_COST") public ApiResponse cost(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CostRequest request) { if (request.currency() != null && !"CNY".equals(request.currency())) throw new IllegalArgumentException("首期仅支持CNY"); - var internal = new FinanceService.CostRequest(null, request.costType(), request.orderId(), null, request.containerId(), null, null, "MANUAL", request.expenseName(), request.amount(), request.voucherFileIds(), request.remark()); java.util.List volumes = java.util.List.of(); + Long containerBookingId = null; if ("CONTAINER_SHARED".equals(request.costType())) { if (request.containerId() == null) throw new IllegalArgumentException("整柜成本必须指定柜次"); var response = costSnapshotClient.loadingPlan(fileToken, request.customerId(), request.containerId()); @@ -180,9 +180,13 @@ public class PlatformFinanceController { || snapshot.orders() == null || snapshot.orders().isEmpty()) { throw new IllegalArgumentException("Firstleg未返回有效柜内订单体积快照"); } + containerBookingId = snapshot.containerBookingId(); volumes = snapshot.orders().stream().map(v -> new FinanceService.ContainerOrderVolume(v.orderId(), - v.inboundId(), snapshot.loadingPlanId(), snapshot.customerId(), v.volumeCbm())).toList(); + v.inboundId(), snapshot.containerBookingId(), snapshot.customerId(), v.volumeCbm())).toList(); } + var internal = new FinanceService.CostRequest(null, request.costType(), request.orderId(), null, + containerBookingId, null, null, "MANUAL", request.expenseName(), request.amount(), + request.voucherFileIds(), request.remark()); return FinanceResponses.success(resourceView(service.createCost(request.customerId(), user.userId(), internal, volumes, key))); } diff --git a/finance/src/main/java/com/baiyue/finance/dto/FinanceContainerSettlementDtos.java b/finance/src/main/java/com/baiyue/finance/dto/FinanceContainerSettlementDtos.java index c6b7ce5b9..1ccc7c162 100644 --- a/finance/src/main/java/com/baiyue/finance/dto/FinanceContainerSettlementDtos.java +++ b/finance/src/main/java/com/baiyue/finance/dto/FinanceContainerSettlementDtos.java @@ -9,7 +9,7 @@ public final class FinanceContainerSettlementDtos { public record ContainerSettlementDetails( long customerId, - long containerId, + long containerBookingId, List settledSourceFeeIds, List costs) {} @@ -20,7 +20,7 @@ public final class FinanceContainerSettlementDtos { String costOrigin, Long sourceFeeId, Integer sourceFeeVersion, - long containerId, + long containerBookingId, String supplierName, String feeItemName, BigDecimal amount, diff --git a/finance/src/main/java/com/baiyue/finance/mapper/FinanceContainerSettlementQueryMapper.java b/finance/src/main/java/com/baiyue/finance/mapper/FinanceContainerSettlementQueryMapper.java index 0844c3247..4e3d05723 100644 --- a/finance/src/main/java/com/baiyue/finance/mapper/FinanceContainerSettlementQueryMapper.java +++ b/finance/src/main/java/com/baiyue/finance/mapper/FinanceContainerSettlementQueryMapper.java @@ -13,14 +13,15 @@ public interface FinanceContainerSettlementQueryMapper { @Select(""" SELECT COUNT(*) AS customerLinkCount FROM ( - SELECT customer_id, container_id FROM fin_fee_source - WHERE customer_id = #{customerId} AND container_id = #{containerId} + SELECT customer_id, container_booking_id AS container_id FROM fin_fee_source + WHERE customer_id = #{customerId} AND container_booking_id = #{containerBookingId} UNION ALL SELECT tenant_id AS customer_id, container_id FROM fin_cost - WHERE tenant_id = #{customerId} AND container_id = #{containerId} + WHERE tenant_id = #{customerId} AND container_id = #{containerBookingId} ) links """) - OwnershipRow ownership(@Param("customerId") long customerId, @Param("containerId") long containerId); + OwnershipRow ownership(@Param("customerId") long customerId, + @Param("containerBookingId") long containerBookingId); @Select(""" SELECT DISTINCT settled.source_fee_id @@ -32,14 +33,14 @@ public interface FinanceContainerSettlementQueryMapper { WHERE s.tenant_id = #{customerId} AND s.status = 'PAID' AND f.customer_id = #{customerId} - AND f.container_id = #{containerId} + AND f.container_booking_id = #{containerBookingId} AND f.source_system = 'FIRSTLEG' AND f.source_type = 'CONTAINER_FEE' UNION ALL SELECT c.source_fee_id FROM fin_cost c WHERE c.tenant_id = #{customerId} - AND c.container_id = #{containerId} + AND c.container_id = #{containerBookingId} AND c.cost_origin = 'FIRSTLEG_DIRECT_PAID' AND c.source_system = 'FIRSTLEG' AND c.source_type = 'CONTAINER_FEE' @@ -49,12 +50,12 @@ public interface FinanceContainerSettlementQueryMapper { ORDER BY settled.source_fee_id """) List settledSourceFeeIds(@Param("customerId") long customerId, - @Param("containerId") long containerId); + @Param("containerBookingId") long containerBookingId); @Select(""" SELECT c.id AS costId, c.cost_no AS costNo, c.cost_type AS costType, c.cost_origin AS costOrigin, c.source_fee_id AS sourceFeeId, c.source_fee_version AS sourceFeeVersion, - c.container_id AS containerId, c.supplier_name_snapshot AS supplierName, + c.container_id AS containerBookingId, c.supplier_name_snapshot AS supplierName, c.fee_item_name AS feeItemName, c.cost_amount AS amount, c.currency_code AS currency, c.status, a.id AS allocationId, a.order_id AS orderId, a.inbound_id AS inboundId, @@ -63,11 +64,11 @@ public interface FinanceContainerSettlementQueryMapper { a.rounding_adjustment AS roundingAdjustment FROM fin_cost c LEFT JOIN fin_cost_allocation a ON a.tenant_id = c.tenant_id AND a.cost_id = c.id - WHERE c.tenant_id = #{customerId} AND c.container_id = #{containerId} + WHERE c.tenant_id = #{customerId} AND c.container_id = #{containerBookingId} ORDER BY c.created_at DESC, c.id DESC, a.id """) List costs(@Param("customerId") long customerId, - @Param("containerId") long containerId); + @Param("containerBookingId") long containerBookingId); final class OwnershipRow { private long customerLinkCount; @@ -83,7 +84,7 @@ public interface FinanceContainerSettlementQueryMapper { private String costOrigin; private Long sourceFeeId; private Integer sourceFeeVersion; - private long containerId; + private long containerBookingId; private String supplierName; private String feeItemName; private BigDecimal amount; @@ -110,8 +111,8 @@ public interface FinanceContainerSettlementQueryMapper { public void setSourceFeeId(Long value) { this.sourceFeeId = value; } public Integer getSourceFeeVersion() { return sourceFeeVersion; } public void setSourceFeeVersion(Integer value) { this.sourceFeeVersion = value; } - public long getContainerId() { return containerId; } - public void setContainerId(long value) { this.containerId = value; } + public long getContainerBookingId() { return containerBookingId; } + public void setContainerBookingId(long value) { this.containerBookingId = value; } public String getSupplierName() { return supplierName; } public void setSupplierName(String value) { this.supplierName = value; } public String getFeeItemName() { return feeItemName; } diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceContainerSettlementQueryService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceContainerSettlementQueryService.java index a0a14b188..beea4c0c2 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinanceContainerSettlementQueryService.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceContainerSettlementQueryService.java @@ -22,21 +22,21 @@ public class FinanceContainerSettlementQueryService { this.mapper = mapper; } - public ContainerSettlementDetails details(long customerId, long containerId) { + public ContainerSettlementDetails details(long customerId, long containerBookingId) { if (customerId <= 0) throw new IllegalArgumentException("客户范围无效"); - if (containerId <= 0) throw new IllegalArgumentException("柜次范围无效"); + if (containerBookingId <= 0) throw new IllegalArgumentException("货柜范围无效"); - var ownership = mapper.ownership(customerId, containerId); + var ownership = mapper.ownership(customerId, containerBookingId); if (ownership == null || ownership.getCustomerLinkCount() == 0) { throw new BizException(ResultCode.NOT_FOUND, "柜次不存在或不属于客户"); } - List settledSourceFeeIds = mapper.settledSourceFeeIds(customerId, containerId).stream() + List settledSourceFeeIds = mapper.settledSourceFeeIds(customerId, containerBookingId).stream() .filter(value -> value != null && value > 0) .distinct() .toList(); - List costs = groupCosts(mapper.costs(customerId, containerId)); - return new ContainerSettlementDetails(customerId, containerId, settledSourceFeeIds, costs); + List costs = groupCosts(mapper.costs(customerId, containerBookingId)); + return new ContainerSettlementDetails(customerId, containerBookingId, settledSourceFeeIds, costs); } private static List groupCosts(List rows) { @@ -60,7 +60,7 @@ public class FinanceContainerSettlementQueryService { private ContainerCostDetail view() { return new ContainerCostDetail(cost.getCostId(), cost.getCostNo(), cost.getCostType(), - cost.getCostOrigin(), cost.getSourceFeeId(), cost.getSourceFeeVersion(), cost.getContainerId(), + cost.getCostOrigin(), cost.getSourceFeeId(), cost.getSourceFeeVersion(), cost.getContainerBookingId(), cost.getSupplierName(), cost.getFeeItemName(), cost.getAmount(), cost.getCurrency(), cost.getStatus(), List.copyOf(allocations)); } diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceFeeSourceEventService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceFeeSourceEventService.java index 6687e7d3f..44580404f 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinanceFeeSourceEventService.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceFeeSourceEventService.java @@ -86,10 +86,11 @@ public class FinanceFeeSourceEventService { snapshot.sourceType(), snapshot.feeId()); jdbc.update("UPDATE fin_cost SET status='VOIDED',voided_at=CURRENT_TIMESTAMP(3),void_reason='来源版本已替代',version=version+1,updated_by=0 WHERE cost_origin='FIRSTLEG_DIRECT_PAID' AND source_system='FIRSTLEG' AND source_type=? AND source_fee_id=? AND status='CONFIRMED'", snapshot.sourceType(), snapshot.feeId()); - jdbc.update("INSERT INTO fin_fee_source(source_system,source_fee_id,source_fee_version,customer_id,source_type,source_no,direction,order_id,inbound_id,container_id,fee_item_code,fee_item_name,amount,currency_code,settlement_mode,paid_at,payment_method,payment_reference_no,payment_voucher_file_ids,source_hash,source_snapshot,status) VALUES ('FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'AVAILABLE')", + jdbc.update("INSERT INTO fin_fee_source(source_system,source_fee_id,source_fee_version,customer_id,source_type,source_no,direction,order_id,inbound_id,container_id,container_booking_id,fee_item_code,fee_item_name,amount,currency_code,settlement_mode,paid_at,payment_method,payment_reference_no,payment_voucher_file_ids,source_hash,source_snapshot,status) VALUES ('FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'AVAILABLE')", snapshot.feeId(), snapshot.feeVersion(), snapshot.customerId(), snapshot.sourceType(), snapshot.inboundCode() == null || snapshot.inboundCode().isBlank() ? snapshot.containerNo() : snapshot.inboundCode(), snapshot.direction(), snapshot.orderId(), snapshot.inboundId(), snapshot.containerId(), + financeContainerBookingId(snapshot), snapshot.feeItemCode(), snapshot.feeItemName(), snapshot.totalAmount(), snapshot.currency(), snapshot.settlementMode(), snapshot.paidAt(), snapshot.paymentMethod(), snapshot.paymentReferenceNo(), voucherJson(snapshot.paymentVoucherFileIds()), @@ -116,7 +117,7 @@ public class FinanceFeeSourceEventService { String costNo = "DIRECT-" + snapshot.sourceType() + "-" + snapshot.feeId() + "-V" + snapshot.feeVersion(); jdbc.update("INSERT INTO fin_cost(tenant_id,cost_no,cost_type,cost_origin,source_system,source_type,source_fee_id,source_fee_version,source_hash,order_id,inbound_id,container_id,supplier_id,supplier_name_snapshot,fee_item_code,fee_item_name,currency_code,cost_amount,voucher_file_ids,paid_at,payment_method,payment_reference_no,status,confirmed_at,created_by,created_by_name,updated_by) VALUES(?,?,'SOURCE_DIRECT_PAID','FIRSTLEG_DIRECT_PAID','FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED',CURRENT_TIMESTAMP(3),0,'系统',0)", snapshot.customerId(), costNo, snapshot.sourceType(), snapshot.feeId(), snapshot.feeVersion(), - snapshot.sourceHash(), snapshot.orderId(), snapshot.inboundId(), snapshot.containerId(), + snapshot.sourceHash(), snapshot.orderId(), snapshot.inboundId(), financeContainerBookingId(snapshot), snapshot.supplierId(), snapshot.supplierName(), snapshot.feeItemCode(), snapshot.feeItemName(), snapshot.currency(), snapshot.totalAmount(), voucherJsonOrEmpty(snapshot.paymentVoucherFileIds()), snapshot.paidAt(), snapshot.paymentMethod(), snapshot.paymentReferenceNo()); @@ -184,7 +185,11 @@ public class FinanceFeeSourceEventService { throw error("FIN-EVT-012", "应收费用不得携带结算方式或已付快照"); } if ("INBOUND_FEE".equals(value.sourceType()) && (value.inboundId() == null || !String.valueOf(value.inboundId()).equals(event.payload().inboundId()))) throw error("FIN-EVT-013", "入库单归属不一致"); - if ("CONTAINER_FEE".equals(value.sourceType()) && (value.containerId() == null || !String.valueOf(value.containerId()).equals(event.payload().containerId()))) throw error("FIN-EVT-014", "柜子归属不一致"); + if ("CONTAINER_FEE".equals(value.sourceType()) && (value.containerId() == null + || !String.valueOf(value.containerId()).equals(event.payload().containerId()) + || value.containerBookingId() == null || value.containerBookingId() <= 0)) { + throw error("FIN-EVT-014", "柜子归属不一致"); + } } private void validatePayableSettlement(FirstlegFeeSourceClient.Snapshot value) { @@ -216,6 +221,10 @@ public class FinanceFeeSourceEventService { return values == null || values.isEmpty() ? "[]" : json(values); } + private static Long financeContainerBookingId(FirstlegFeeSourceClient.Snapshot snapshot) { + return "CONTAINER_FEE".equals(snapshot.sourceType()) ? snapshot.containerBookingId() : null; + } + private String json(Object value) { try { return objectMapper.writeValueAsString(value); } catch (JsonProcessingException e) { throw error("FIN-EVT-001", "事件序列化失败"); } diff --git a/finance/src/main/resources/db/finance.sql b/finance/src/main/resources/db/finance.sql index b4c86dab1..9ce08b473 100644 --- a/finance/src/main/resources/db/finance.sql +++ b/finance/src/main/resources/db/finance.sql @@ -229,7 +229,7 @@ CREATE TABLE IF NOT EXISTS fin_cost ( source_hash VARCHAR(71) NULL COMMENT '来源不可变快照SHA-256,兼容sha256前缀格式', order_id BIGINT UNSIGNED NULL COMMENT '成本归属的头程订单ID,单票成本必填', inbound_id BIGINT UNSIGNED NULL COMMENT '成本关联的入库单ID,可为空', - container_id BIGINT UNSIGNED NULL COMMENT 'Firstleg装柜计划ID,整柜公共成本必填', + container_id BIGINT UNSIGNED NULL COMMENT 'Firstleg真实货柜containerBookingId,整柜公共成本必填', warehouse_id BIGINT UNSIGNED NULL COMMENT '业务直接已付成本关联仓库ID,可为空', supplier_id BIGINT UNSIGNED NULL COMMENT 'Warehouse公共往来单位或供应商引用ID', supplier_name_snapshot VARCHAR(200) NULL COMMENT '登记成本时供应商或成本对象名称快照', diff --git a/finance/src/main/resources/openapi/finance-container-settlement-v1.yaml b/finance/src/main/resources/openapi/finance-container-settlement-v1.yaml index 067491414..c7b0a8f0b 100644 --- a/finance/src/main/resources/openapi/finance-container-settlement-v1.yaml +++ b/finance/src/main/resources/openapi/finance-container-settlement-v1.yaml @@ -10,7 +10,7 @@ paths: get: summary: 按客户和柜次查询已结算柜费及成本分摊 description: >- - customerId 与 containerId 必须在 Finance 的费用来源或成本快照中存在归属关系。 + customerId 与 containerBookingId 必须在 Finance 的费用来源或成本快照中存在归属关系。 服务端固定执行三条集合 SQL,不按结算单或成本逐项查询;响应不包含整张结算单、 其他客户数据、付款凭证或内部备注。 x-permissions-all-of: @@ -25,10 +25,10 @@ paths: in: query required: true schema: {$ref: '#/components/schemas/Id'} - - name: containerId + - name: containerBookingId in: query required: true - description: Firstleg 装柜计划 ID。 + description: Firstleg 真实货柜 containerBookingId,不是装柜计划 loadingPlanId。 schema: {$ref: '#/components/schemas/Id'} responses: '200': @@ -63,10 +63,10 @@ components: timestamp: {type: string, format: date-time} ContainerSettlementDetails: type: object - required: [customerId, containerId, settledSourceFeeIds, costs] + required: [customerId, containerBookingId, settledSourceFeeIds, costs] properties: customerId: {$ref: '#/components/schemas/Id'} - containerId: {$ref: '#/components/schemas/Id'} + containerBookingId: {$ref: '#/components/schemas/Id'} settledSourceFeeIds: type: array uniqueItems: true @@ -76,7 +76,7 @@ components: items: {$ref: '#/components/schemas/ContainerCostDetail'} ContainerCostDetail: type: object - required: [costId, costNo, costType, costOrigin, containerId, feeItemName, amount, currency, status, allocations] + required: [costId, costNo, costType, costOrigin, containerBookingId, feeItemName, amount, currency, status, allocations] properties: costId: {$ref: '#/components/schemas/Id'} costNo: {type: string} @@ -84,7 +84,7 @@ components: costOrigin: {type: string, enum: [MANUAL, FIRSTLEG_DIRECT_PAID, BUSINESS_DIRECT_PAID]} sourceFeeId: {$ref: '#/components/schemas/Id'} sourceFeeVersion: {type: integer, minimum: 1} - containerId: {$ref: '#/components/schemas/Id'} + containerBookingId: {$ref: '#/components/schemas/Id'} supplierName: {type: string, nullable: true} feeItemName: {type: string} amount: {$ref: '#/components/schemas/Money'} diff --git a/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementOpenApiContractTest.java b/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementOpenApiContractTest.java index 9cacd2197..09c7b29dd 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementOpenApiContractTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementOpenApiContractTest.java @@ -22,6 +22,8 @@ class FinanceContainerSettlementOpenApiContractTest { .contains("platform:finance:settlement-ar:list") .contains("platform:finance:settlement-ap:list") .contains("x-query-count: 3 fixed set-based SQL statements") - .contains("settledSourceFeeIds:", "costs:", "allocations:"); + .contains("containerBookingId:", "不是装柜计划 loadingPlanId", + "settledSourceFeeIds:", "costs:", "allocations:") + .doesNotContain("- name: containerId"); } } diff --git a/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementQueryMapperContractTest.java b/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementQueryMapperContractTest.java index 1d0e0c574..db1107348 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementQueryMapperContractTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementQueryMapperContractTest.java @@ -23,7 +23,8 @@ class FinanceContainerSettlementQueryMapperContractTest { .reduce("", (left, right) -> left + "\n" + right); assertThat(sql).contains("JOIN fin_settlement_line", "JOIN fin_fee_source", "LEFT JOIN fin_cost_allocation", "c.tenant_id = #{customerId}", - "c.container_id = #{containerId}"); + "f.container_booking_id = #{containerBookingId}", + "c.container_id = #{containerBookingId}"); assertThat(sql).doesNotContain("${"); } } diff --git a/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementQueryServiceTest.java b/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementQueryServiceTest.java index 5514a6f31..3dc4f94ff 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementQueryServiceTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceContainerSettlementQueryServiceTest.java @@ -33,7 +33,7 @@ class FinanceContainerSettlementQueryServiceTest { var result = new FinanceContainerSettlementQueryService(mapper).details(101L, 301L); assertThat(result.customerId()).isEqualTo(101L); - assertThat(result.containerId()).isEqualTo(301L); + assertThat(result.containerBookingId()).isEqualTo(301L); assertThat(result.settledSourceFeeIds()).containsExactly(901L, 902L); assertThat(result.costs()).hasSize(2); assertThat(result.costs().get(0).costId()).isEqualTo(501L); @@ -79,14 +79,14 @@ class FinanceContainerSettlementQueryServiceTest { return row; } - private static CostAllocationRow row(long costId, long containerId, Long allocationId, + private static CostAllocationRow row(long costId, long containerBookingId, Long allocationId, Long orderId, boolean roundingAdjustment) { var row = new CostAllocationRow(); row.setCostId(costId); row.setCostNo("COST-" + costId); row.setCostType("CONTAINER_SHARED"); row.setCostOrigin("MANUAL"); - row.setContainerId(containerId); + row.setContainerBookingId(containerBookingId); row.setSupplierName("供应商"); row.setFeeItemName("柜费"); row.setAmount(new BigDecimal("100.00")); diff --git a/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceEventServiceTest.java b/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceEventServiceTest.java index 3f44aba12..4bee25015 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceEventServiceTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceEventServiceTest.java @@ -62,6 +62,31 @@ class FinanceFeeSourceEventServiceTest { assertThat(service.consume(event)).isEqualTo(101L); verify(firstleg).container("test-token", 41, 31, 1); verify(jdbc).update(contains("status='SUPERSEDED'"), eq("CONTAINER_FEE"), eq(31L)); + ArgumentCaptor values = ArgumentCaptor.forClass(Object[].class); + verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture()); + assertThat(values.getValue()[8]).as("fin_fee_source.container_id keeps loadingPlanId") + .isEqualTo(41L); + assertThat(values.getValue()[9]).as("fin_fee_source.container_booking_id stores physical container") + .isEqualTo(61L); + } + + @Test + void rejectsContainerFeeSnapshotWithoutPhysicalContainerBookingId() { + FinanceFeeSourceEvent event = containerEvent("container-missing-booking", "FIRSTLEG_FEE_PUBLISHED", 1, 7); + when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("FIRSTLEG"), + eq("container-missing-booking"))).thenReturn(List.of()); + var snapshot = new FirstlegFeeSourceClient.Snapshot(7, "CONTAINER_FEE", 31, 1, + null, null, null, 41L, null, "C-41", 7, "客户", null, null, + null, null, null, null, null, List.of(), 61L, "CUSTOMS", "清关费", + "RECEIVABLE", LocalDate.of(2026, 8, 11), "CNY", new BigDecimal("100.00"), + "APPROVED", "PUBLISHED", LocalDateTime.of(2026, 8, 11, 1, 0), + BigDecimal.ONE, "SHIPMENT", BigDecimal.TEN, BigDecimal.ONE, "source-hash", 1); + when(firstleg.container("test-token", 41, 31, 1)).thenReturn(ApiResponse.success(snapshot)); + + assertThatThrownBy(() -> service.consume(event)) + .isInstanceOf(FinanceEventException.class) + .extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-EVT-014"); + verify(jdbc, never()).update(startsWith("INSERT INTO fin_fee_source"), any(Object[].class)); } @Test @@ -83,7 +108,7 @@ class FinanceFeeSourceEventServiceTest { verify(jdbc, never()).update(startsWith("INSERT INTO fin_cost"), any(Object[].class)); ArgumentCaptor values = ArgumentCaptor.forClass(Object[].class); verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture()); - assertThat(values.getValue()[19].toString()) + assertThat(values.getValue()[20].toString()) .contains("\"supplierId\":71", "\"supplierName\":\"现有供应商\"", "\"supplierCategory\":\"TRUCKING\""); } @@ -121,9 +146,9 @@ class FinanceFeeSourceEventServiceTest { ArgumentCaptor values = ArgumentCaptor.forClass(Object[].class); verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture()); - assertThat(values.getValue()[13]).isEqualTo("DIRECT_PAID"); - assertThat(values.getValue()[15]).isEqualTo("BANK_TRANSFER"); - assertThat(values.getValue()[17]).isEqualTo("[901]"); + assertThat(values.getValue()[14]).isEqualTo("DIRECT_PAID"); + assertThat(values.getValue()[16]).isEqualTo("BANK_TRANSFER"); + assertThat(values.getValue()[18]).isEqualTo("[901]"); ArgumentCaptor costValues = ArgumentCaptor.forClass(Object[].class); verify(jdbc).update(startsWith("INSERT INTO fin_cost"), costValues.capture()); assertThat(costValues.getValue()[0]).isEqualTo(7L); @@ -131,6 +156,8 @@ class FinanceFeeSourceEventServiceTest { assertThat(costValues.getValue()[2]).isEqualTo("CONTAINER_FEE"); assertThat(costValues.getValue()[3]).isEqualTo(31L); assertThat(costValues.getValue()[4]).isEqualTo(1); + assertThat(costValues.getValue()[8]).as("fin_cost.container_id stores containerBookingId") + .isEqualTo(61L); assertThat(costValues.getValue()[15]).isEqualTo("[901]"); assertThat(costValues.getValue()[17]).isEqualTo("BANK_TRANSFER"); assertThat(costValues.getValue()[18]).isEqualTo("BANK-001"); @@ -284,7 +311,7 @@ class FinanceFeeSourceEventServiceTest { Long inboundId, Long containerId, String publicationStatus, BigDecimal amount) { return new FirstlegFeeSourceClient.Snapshot(customerId, sourceType, 31, version, inboundId, - inboundId == null ? null : "IB-21", 51L, containerId, null, + inboundId == null ? null : "IB-21", 51L, containerId, containerId == null ? null : 61L, containerId == null ? null : "C-41", customerId, "客户", null, null, null, null, null, null, null, List.of(), 61L, "CUSTOMS", "清关费", "RECEIVABLE", LocalDate.of(2026, 8, 11), "CNY", amount, diff --git a/finance/src/test/java/com/baiyue/finance/PlatformFinanceControllerBehaviorTest.java b/finance/src/test/java/com/baiyue/finance/PlatformFinanceControllerBehaviorTest.java index bd04c047b..242a99628 100644 --- a/finance/src/test/java/com/baiyue/finance/PlatformFinanceControllerBehaviorTest.java +++ b/finance/src/test/java/com/baiyue/finance/PlatformFinanceControllerBehaviorTest.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -55,9 +56,13 @@ class PlatformFinanceControllerBehaviorTest { controller.cost(user, "cost-key", request); verify(snapshotClient).loadingPlan("internal-token", 10L, 88L); - verify(service).createCost(eq(10L), eq(7L), any(), eq(List.of( - new FinanceService.ContainerOrderVolume(101L, 1001L, 88L, 10L, new BigDecimal("1.25")), - new FinanceService.ContainerOrderVolume(102L, 1002L, 88L, 10L, new BigDecimal("2.75")))), eq("cost-key")); + org.mockito.ArgumentCaptor cost = + org.mockito.ArgumentCaptor.forClass(FinanceService.CostRequest.class); + verify(service).createCost(eq(10L), eq(7L), cost.capture(), eq(List.of( + new FinanceService.ContainerOrderVolume(101L, 1001L, 99L, 10L, new BigDecimal("1.25")), + new FinanceService.ContainerOrderVolume(102L, 1002L, 99L, 10L, new BigDecimal("2.75")))), eq("cost-key")); + assertThat(cost.getValue().containerId()).as("Finance persists the physical containerBookingId") + .isEqualTo(99L); } @Test