From 4562e3f1d1532c1c43e67577a8f0ee44bd12974e Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 12 Aug 2026 16:58:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/sql/头程数据库统一增量-MySQL8.sql | 17 +++++++-- .../FinanceFeeSourceEventServiceTest.java | 21 +++++++++++ .../firstleg/dto/ContainerFeeDetailDtos.java | 6 +++- .../entity/ContainerFeeDetailEntity.java | 2 ++ .../service/ContainerFeeDetailService.java | 8 +++++ .../FinanceFeeSourceSnapshotService.java | 4 ++- .../db/finance-fee-source-migration.sql | 15 ++++++-- .../controller/FeeManagementContractTest.java | 12 +++++++ ...FinanceFeeSourceMigrationContractTest.java | 3 +- .../ContainerFeeDetailServiceTest.java | 35 ++++++++++++++++++- .../FinanceFeeSourceSnapshotServiceTest.java | 31 ++++++++++++++++ 11 files changed, 146 insertions(+), 8 deletions(-) diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index 17ce35f6c..4a585a17b 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -10371,8 +10371,6 @@ CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_by', CALL firstleg_finance_add_column('fl_inbound_outbox', 'last_error', "VARCHAR(500) NULL COMMENT '最近一次投递失败摘要,不记录令牌' AFTER locked_by"); -DROP PROCEDURE IF EXISTS firstleg_finance_add_column; - DROP PROCEDURE IF EXISTS firstleg_finance_add_index; DELIMITER $$ CREATE PROCEDURE firstleg_finance_add_index( @@ -10407,6 +10405,8 @@ CREATE TABLE IF NOT EXISTS fl_container_fee_detail ( fee_item_code_snapshot VARCHAR(50) NOT NULL COMMENT '费用项目稳定编码快照', fee_item_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用项目名称快照', direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付', + partner_id BIGINT UNSIGNED NULL COMMENT '应付往来单位ID快照;应收为空', + partner_name VARCHAR(200) NULL COMMENT '应付往来单位名称快照;应收为空', business_date DATE NOT NULL COMMENT '费用业务日期', amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额', currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种,首期固定CNY', @@ -10433,6 +10433,12 @@ CREATE TABLE IF NOT EXISTS fl_container_fee_detail ( CONSTRAINT chk_container_fee_source CHECK (source_status IN ('ACTIVE', 'VOIDED')) ) ENGINE=InnoDB COMMENT='柜次级人工应收应付费用来源;审核通过且参与结算后同步Finance'; +CALL firstleg_finance_add_column('fl_container_fee_detail', 'partner_id', + "BIGINT UNSIGNED NULL COMMENT '应付往来单位ID快照;应收为空' AFTER direction"); +CALL firstleg_finance_add_column('fl_container_fee_detail', 'partner_name', + "VARCHAR(200) NULL COMMENT '应付往来单位名称快照;应收为空' AFTER partner_id"); +DROP PROCEDURE IF EXISTS firstleg_finance_add_column; + DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema; DELIMITER $$ CREATE PROCEDURE firstleg_finance_assert_schema() @@ -10447,6 +10453,11 @@ BEGIN WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用表不存在'; END IF; + IF (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail' + AND column_name IN ('partner_id', 'partner_name')) <> 2 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用往来单位快照字段不完整'; + END IF; END$$ DELIMITER ; CALL firstleg_finance_assert_schema(); @@ -11532,6 +11543,8 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'created CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'created_by', '创建人ID'); CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'currency', '币种,首期固定CNY'); CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'direction', '费用方向:RECEIVABLE应收、PAYABLE应付'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'partner_id', '应付往来单位ID快照;应收为空'); +CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'partner_name', '应付往来单位名称快照;应收为空'); CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'fee_item_code_snapshot', '费用项目稳定编码快照'); CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'fee_item_id', '平台公共费用项目ID'); CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'fee_item_name_snapshot', '费用项目名称快照'); diff --git a/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceEventServiceTest.java b/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceEventServiceTest.java index 44f4a14cd..aee0b8c8d 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceEventServiceTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceFeeSourceEventServiceTest.java @@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import org.mockito.ArgumentCaptor; class FinanceFeeSourceEventServiceTest { private final JdbcTemplate jdbc = mock(JdbcTemplate.class); @@ -58,6 +59,26 @@ class FinanceFeeSourceEventServiceTest { verify(jdbc).update(contains("status='SUPERSEDED'"), eq(31L)); } + @Test + void importsContainerPayableWithCounterpartySnapshotForApSettlement() { + FinanceFeeSourceEvent event = containerEvent("container-payable-1", "FIRSTLEG_FEE_PUBLISHED", 1, 7); + givenNewEvent(); + var snapshot = new FirstlegFeeSourceClient.Snapshot(7, "CONTAINER_FEE", 31, 1, + null, null, null, 41L, 61L, "C-41", 7, "客户", 71L, "现有往来单位", + 61L, "TRUCKING", "拖车费", "PAYABLE", LocalDate.of(2026, 8, 11), + "CNY", new BigDecimal("300.00"), "APPROVED", "PUBLISHED", true, + LocalDateTime.of(2026, 8, 11, 1, 0), null, null, null, null, + "source-hash-payable", 1); + when(firstleg.container("test-token", 41, 31)).thenReturn(ApiResponse.success(snapshot)); + + assertThat(service.consume(event)).isEqualTo(101L); + + ArgumentCaptor values = ArgumentCaptor.forClass(Object[].class); + verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture()); + assertThat(values.getValue()[14].toString()) + .contains("\"partnerId\":71", "\"partnerName\":\"现有往来单位\""); + } + @Test void adjustmentCreatesNewImmutableVersionAndSupersedesAvailableVersion() { FinanceFeeSourceEvent event = inboundEvent("adjusted-2", "FIRSTLEG_FEE_ADJUSTED", 2, 7); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerFeeDetailDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerFeeDetailDtos.java index 7acd6fa3a..bdef4e629 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerFeeDetailDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerFeeDetailDtos.java @@ -21,6 +21,8 @@ public final class ContainerFeeDetailDtos { @NotNull @DecimalMin("0.00") BigDecimal amount, @NotBlank @Pattern(regexp = "CNY") String currency, @NotNull Boolean settlementIncluded, + Long partnerId, + @Size(max = 200) String partnerName, @Size(max = 500) String remark ) { } @@ -33,11 +35,13 @@ public final class ContainerFeeDetailDtos { @NotNull @DecimalMin("0.00") BigDecimal amount, @NotBlank @Pattern(regexp = "CNY") String currency, @NotNull Boolean settlementIncluded, + Long partnerId, + @Size(max = 200) String partnerName, @Size(max = 500) String remark ) { public SaveRequest values() { return new SaveRequest(feeItemId, direction, businessDate, amount, currency, - settlementIncluded, remark); + settlementIncluded, partnerId, partnerName, remark); } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerFeeDetailEntity.java b/firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerFeeDetailEntity.java index a3cd100c8..5a61d5f9d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerFeeDetailEntity.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerFeeDetailEntity.java @@ -25,6 +25,8 @@ public class ContainerFeeDetailEntity { private String feeItemCodeSnapshot; private String feeItemNameSnapshot; private String direction; + private Long partnerId; + private String partnerName; private LocalDate businessDate; private BigDecimal amount; private String currency; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerFeeDetailService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerFeeDetailService.java index dd3a0b6a5..939ffc29b 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerFeeDetailService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerFeeDetailService.java @@ -13,6 +13,7 @@ import com.baiyue.firstleg.repository.SmartLoadingRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; import java.math.RoundingMode; import java.time.LocalDateTime; @@ -140,10 +141,17 @@ public class ContainerFeeDetailService { private static void apply(ContainerFeeDetailEntity value, FeeItemEntity item, SaveRequest request, long operatorId) { + if ("PAYABLE".equals(request.direction()) + && (request.partnerId() == null || request.partnerId() <= 0 + || !StringUtils.hasText(request.partnerName()))) { + throw new BizException(ResultCode.VALIDATION_FAILED, "应付柜次费用必须选择有效往来单位"); + } value.setFeeItemId(item.getId()); value.setFeeItemCodeSnapshot(item.getFeeCode()); value.setFeeItemNameSnapshot(item.getFeeTypeName()); value.setDirection(request.direction()); + value.setPartnerId("PAYABLE".equals(request.direction()) ? request.partnerId() : null); + value.setPartnerName("PAYABLE".equals(request.direction()) ? request.partnerName().trim() : null); value.setBusinessDate(request.businessDate()); value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP)); value.setCurrency(request.currency()); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotService.java index 4de08f7b8..8036c1d8c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotService.java @@ -61,11 +61,13 @@ public class FinanceFeeSourceSnapshotService { String canonical = String.join("|", "CONTAINER_FEE", String.valueOf(feeId), String.valueOf(version), String.valueOf(detail.getTenantId()), String.valueOf(containerId), nullSafe(detail.getFeeItemCodeSnapshot()), nullSafe(detail.getDirection()), + nullSafe(detail.getPartnerId()), nullSafe(detail.getPartnerName()), detail.getAmount().toPlainString(), nullSafe(detail.getCurrency()), String.valueOf(detail.getSettlementIncluded())); return new FinanceFeeSourceSnapshot(detail.getTenantId(), "CONTAINER_FEE", feeId, version, null, null, null, containerId, detail.getContainerBookingId(), - detail.getContainerNo(), detail.getTenantId(), null, null, null, detail.getFeeItemId(), + detail.getContainerNo(), detail.getTenantId(), null, detail.getPartnerId(), + detail.getPartnerName(), detail.getFeeItemId(), detail.getFeeItemCodeSnapshot(), detail.getFeeItemNameSnapshot(), detail.getDirection(), detail.getBusinessDate(), detail.getCurrency(), detail.getAmount(), detail.getReviewStatus(), publicationStatus, Boolean.TRUE.equals(detail.getSettlementIncluded()), diff --git a/firstleg/src/main/resources/db/finance-fee-source-migration.sql b/firstleg/src/main/resources/db/finance-fee-source-migration.sql index 5f3c18f25..422cdff37 100644 --- a/firstleg/src/main/resources/db/finance-fee-source-migration.sql +++ b/firstleg/src/main/resources/db/finance-fee-source-migration.sql @@ -37,8 +37,6 @@ CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_by', CALL firstleg_finance_add_column('fl_inbound_outbox', 'last_error', "VARCHAR(500) NULL COMMENT '最近一次投递失败摘要,不记录令牌' AFTER locked_by"); -DROP PROCEDURE IF EXISTS firstleg_finance_add_column; - DROP PROCEDURE IF EXISTS firstleg_finance_add_index; DELIMITER $$ CREATE PROCEDURE firstleg_finance_add_index( @@ -73,6 +71,8 @@ CREATE TABLE IF NOT EXISTS fl_container_fee_detail ( fee_item_code_snapshot VARCHAR(50) NOT NULL COMMENT '费用项目稳定编码快照', fee_item_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用项目名称快照', direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付', + partner_id BIGINT UNSIGNED NULL COMMENT '应付往来单位ID快照;应收为空', + partner_name VARCHAR(200) NULL COMMENT '应付往来单位名称快照;应收为空', business_date DATE NOT NULL COMMENT '费用业务日期', amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额', currency CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种,首期固定CNY', @@ -99,6 +99,12 @@ CREATE TABLE IF NOT EXISTS fl_container_fee_detail ( CONSTRAINT chk_container_fee_source CHECK (source_status IN ('ACTIVE', 'VOIDED')) ) ENGINE=InnoDB COMMENT='柜次级人工应收应付费用来源;审核通过且参与结算后同步Finance'; +CALL firstleg_finance_add_column('fl_container_fee_detail', 'partner_id', + "BIGINT UNSIGNED NULL COMMENT '应付往来单位ID快照;应收为空' AFTER direction"); +CALL firstleg_finance_add_column('fl_container_fee_detail', 'partner_name', + "VARCHAR(200) NULL COMMENT '应付往来单位名称快照;应收为空' AFTER partner_id"); +DROP PROCEDURE IF EXISTS firstleg_finance_add_column; + DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema; DELIMITER $$ CREATE PROCEDURE firstleg_finance_assert_schema() @@ -113,6 +119,11 @@ BEGIN WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用表不存在'; END IF; + IF (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail' + AND column_name IN ('partner_id', 'partner_name')) <> 2 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用往来单位快照字段不完整'; + END IF; END$$ DELIMITER ; CALL firstleg_finance_assert_schema(); diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java index e229728ff..39d91aa30 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/FeeManagementContractTest.java @@ -7,6 +7,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.yaml.snakeyaml.Yaml; import java.lang.reflect.Method; +import java.lang.reflect.RecordComponent; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; @@ -39,6 +40,17 @@ class FeeManagementContractTest { assertPermission("delete", "firstleg:fee-item:delete"); } + @Test + @DisplayName("柜次费用新增和调整请求均携带往来单位快照") + void containerFeeRequestsCarryCounterpartySnapshot() { + for (Class requestType : List.of( + com.baiyue.firstleg.dto.ContainerFeeDetailDtos.SaveRequest.class, + com.baiyue.firstleg.dto.ContainerFeeDetailDtos.AdjustRequest.class)) { + assertThat(Arrays.stream(requestType.getRecordComponents()).map(RecordComponent::getName)) + .contains("partnerId", "partnerName"); + } + } + @Test @DisplayName("平台费用项目接口不接受客户作用域") void platformFeeItemEndpointsDoNotExposeAccountId() { diff --git a/firstleg/src/test/java/com/baiyue/firstleg/repository/FinanceFeeSourceMigrationContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/repository/FinanceFeeSourceMigrationContractTest.java index 8f8813308..f833b2cf3 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/repository/FinanceFeeSourceMigrationContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/repository/FinanceFeeSourceMigrationContractTest.java @@ -16,7 +16,8 @@ class FinanceFeeSourceMigrationContractTest { assertThat(sql).contains("information_schema.columns", "event_key", "payload_hash", "uk_inbound_outbox_event_key", "next_retry_at", "locked_at", "locked_by", "idx_inbound_outbox_finance_relay", "CREATE TABLE IF NOT EXISTS fl_container_fee_detail", - "settlement_included", "source_version", "RECEIVABLE", "PAYABLE", + "settlement_included", "source_version", "partner_id", "partner_name", + "firstleg_finance_add_column('fl_container_fee_detail', 'partner_id'", "RECEIVABLE", "PAYABLE", "chk_container_fee_currency", "currency = 'CNY'", "firstleg_finance_assert_schema"); } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerFeeDetailServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerFeeDetailServiceTest.java index 9d6c7712c..4227144e2 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerFeeDetailServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/ContainerFeeDetailServiceTest.java @@ -85,6 +85,38 @@ class ContainerFeeDetailServiceTest { verify(repository).adjust(captor.capture(), eq(1)); assertThat(captor.getValue().getSourceVersion()).isEqualTo(2); assertThat(captor.getValue().getReviewStatus()).isEqualTo("PENDING"); + assertThat(captor.getValue().getPartnerId()).isEqualTo(71L); + assertThat(captor.getValue().getPartnerName()).isEqualTo("现有往来单位"); + } + + @Test + void payableRequiresCounterpartySnapshot() { + ownedPlan(); + when(feeItems.findItem(11L)).thenReturn(item()); + + var missingPartner = new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE", + LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY", + true, null, null, "整柜拖车费"); + + assertThatThrownBy(() -> service.create(10001L, 41L, 9L, missingPartner)) + .isInstanceOf(BizException.class).hasMessageContaining("必须选择有效往来单位"); + verify(repository, never()).insert(any()); + } + + @Test + void receivableClearsCounterpartyFields() { + ownedPlan(); + when(feeItems.findItem(11L)).thenReturn(item()); + + var request = new ContainerFeeDetailDtos.SaveRequest(11L, "RECEIVABLE", + LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY", + true, 71L, "不应保留", "整柜操作费"); + service.create(10001L, 41L, 9L, request); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ContainerFeeDetailEntity.class); + verify(repository).insert(captor.capture()); + assertThat(captor.getValue().getPartnerId()).isNull(); + assertThat(captor.getValue().getPartnerName()).isNull(); } private void ownedPlan() { @@ -101,6 +133,7 @@ class ContainerFeeDetailServiceTest { value.setId(11L); value.setFeeCode("TRUCKING"); value.setFeeTypeName("拖车费"); + value.setWarehouseReceivable(true); value.setWarehousePayable(true); value.setStatus("ACTIVE"); return value; @@ -109,6 +142,6 @@ class ContainerFeeDetailServiceTest { private static ContainerFeeDetailDtos.SaveRequest request(boolean settlementIncluded) { return new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE", LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY", - settlementIncluded, "整柜拖车费"); + settlementIncluded, 71L, " 现有往来单位 ", "整柜拖车费"); } } diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotServiceTest.java index 52b431f68..5d334f197 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/FinanceFeeSourceSnapshotServiceTest.java @@ -3,6 +3,7 @@ package com.baiyue.firstleg.service; import com.baiyue.common.core.exception.BizException; import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot; import com.baiyue.firstleg.entity.FeeItemEntity; +import com.baiyue.firstleg.entity.ContainerFeeDetailEntity; import com.baiyue.firstleg.entity.InboundFeeDetailEntity; import com.baiyue.firstleg.entity.InboundOrderEntity; import com.baiyue.firstleg.repository.ContainerFeeDetailRepository; @@ -68,4 +69,34 @@ class FinanceFeeSourceSnapshotServiceTest { assertThatThrownBy(() -> service.inbound(21L, 31L)) .isInstanceOf(BizException.class).hasMessageContaining("不存在"); } + + @Test + void containerSnapshotPublishesPayableCounterpartyIdentity() { + ContainerFeeDetailEntity detail = new ContainerFeeDetailEntity(); + detail.setId(51L); + detail.setTenantId(10001L); + detail.setLoadingPlanId(41L); + detail.setContainerBookingId(61L); + detail.setContainerNo("CONT-41"); + detail.setFeeItemId(11L); + detail.setFeeItemCodeSnapshot("TRUCKING"); + detail.setFeeItemNameSnapshot("拖车费"); + detail.setDirection("PAYABLE"); + detail.setPartnerId(71L); + detail.setPartnerName("现有往来单位"); + detail.setBusinessDate(java.time.LocalDate.of(2026, 8, 12)); + detail.setAmount(new BigDecimal("300.00")); + detail.setCurrency("CNY"); + detail.setReviewStatus("APPROVED"); + detail.setSourceStatus("ACTIVE"); + detail.setSourceVersion(2); + detail.setSettlementIncluded(true); + when(containerFees.find(41L, 51L)).thenReturn(detail); + + FinanceFeeSourceSnapshot snapshot = service.container(41L, 51L); + + assertThat(snapshot.partnerId()).isEqualTo(71L); + assertThat(snapshot.partnerName()).isEqualTo("现有往来单位"); + assertThat(snapshot.sourceHash()).matches("[0-9a-f]{64}"); + } }