This commit is contained in:
2026-08-12 16:58:45 +08:00
parent 600f3b7861
commit 4562e3f1d1
11 changed files with 146 additions and 8 deletions
@@ -10371,8 +10371,6 @@ CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_by',
CALL firstleg_finance_add_column('fl_inbound_outbox', 'last_error', CALL firstleg_finance_add_column('fl_inbound_outbox', 'last_error',
"VARCHAR(500) NULL COMMENT '最近一次投递失败摘要,不记录令牌' AFTER locked_by"); "VARCHAR(500) NULL COMMENT '最近一次投递失败摘要,不记录令牌' AFTER locked_by");
DROP PROCEDURE IF EXISTS firstleg_finance_add_column;
DROP PROCEDURE IF EXISTS firstleg_finance_add_index; DROP PROCEDURE IF EXISTS firstleg_finance_add_index;
DELIMITER $$ DELIMITER $$
CREATE PROCEDURE firstleg_finance_add_index( 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_code_snapshot VARCHAR(50) NOT NULL COMMENT '费用项目稳定编码快照',
fee_item_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用项目名称快照', fee_item_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用项目名称快照',
direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付', 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 '费用业务日期', business_date DATE NOT NULL COMMENT '费用业务日期',
amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额', amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额',
currency CHAR(3) NOT NULL DEFAULT 'CNY' 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')) CONSTRAINT chk_container_fee_source CHECK (source_status IN ('ACTIVE', 'VOIDED'))
) ENGINE=InnoDB COMMENT='柜次级人工应收应付费用来源;审核通过且参与结算后同步Finance'; ) 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; DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema;
DELIMITER $$ DELIMITER $$
CREATE PROCEDURE firstleg_finance_assert_schema() CREATE PROCEDURE firstleg_finance_assert_schema()
@@ -10447,6 +10453,11 @@ BEGIN
WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail') THEN WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用表不存在'; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用表不存在';
END IF; 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$$ END$$
DELIMITER ; DELIMITER ;
CALL firstleg_finance_assert_schema(); 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', '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', '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', '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_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_id', '平台公共费用项目ID');
CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'fee_item_name_snapshot', '费用项目名称快照'); CALL queue_column_comment('baiyue_firstleg', 'fl_container_fee_detail', 'fee_item_name_snapshot', '费用项目名称快照');
@@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import org.mockito.ArgumentCaptor;
class FinanceFeeSourceEventServiceTest { class FinanceFeeSourceEventServiceTest {
private final JdbcTemplate jdbc = mock(JdbcTemplate.class); private final JdbcTemplate jdbc = mock(JdbcTemplate.class);
@@ -58,6 +59,26 @@ class FinanceFeeSourceEventServiceTest {
verify(jdbc).update(contains("status='SUPERSEDED'"), eq(31L)); 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<Object[]> 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 @Test
void adjustmentCreatesNewImmutableVersionAndSupersedesAvailableVersion() { void adjustmentCreatesNewImmutableVersionAndSupersedesAvailableVersion() {
FinanceFeeSourceEvent event = inboundEvent("adjusted-2", "FIRSTLEG_FEE_ADJUSTED", 2, 7); FinanceFeeSourceEvent event = inboundEvent("adjusted-2", "FIRSTLEG_FEE_ADJUSTED", 2, 7);
@@ -21,6 +21,8 @@ public final class ContainerFeeDetailDtos {
@NotNull @DecimalMin("0.00") BigDecimal amount, @NotNull @DecimalMin("0.00") BigDecimal amount,
@NotBlank @Pattern(regexp = "CNY") String currency, @NotBlank @Pattern(regexp = "CNY") String currency,
@NotNull Boolean settlementIncluded, @NotNull Boolean settlementIncluded,
Long partnerId,
@Size(max = 200) String partnerName,
@Size(max = 500) String remark @Size(max = 500) String remark
) { ) {
} }
@@ -33,11 +35,13 @@ public final class ContainerFeeDetailDtos {
@NotNull @DecimalMin("0.00") BigDecimal amount, @NotNull @DecimalMin("0.00") BigDecimal amount,
@NotBlank @Pattern(regexp = "CNY") String currency, @NotBlank @Pattern(regexp = "CNY") String currency,
@NotNull Boolean settlementIncluded, @NotNull Boolean settlementIncluded,
Long partnerId,
@Size(max = 200) String partnerName,
@Size(max = 500) String remark @Size(max = 500) String remark
) { ) {
public SaveRequest values() { public SaveRequest values() {
return new SaveRequest(feeItemId, direction, businessDate, amount, currency, return new SaveRequest(feeItemId, direction, businessDate, amount, currency,
settlementIncluded, remark); settlementIncluded, partnerId, partnerName, remark);
} }
} }
@@ -25,6 +25,8 @@ public class ContainerFeeDetailEntity {
private String feeItemCodeSnapshot; private String feeItemCodeSnapshot;
private String feeItemNameSnapshot; private String feeItemNameSnapshot;
private String direction; private String direction;
private Long partnerId;
private String partnerName;
private LocalDate businessDate; private LocalDate businessDate;
private BigDecimal amount; private BigDecimal amount;
private String currency; private String currency;
@@ -13,6 +13,7 @@ import com.baiyue.firstleg.repository.SmartLoadingRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -140,10 +141,17 @@ public class ContainerFeeDetailService {
private static void apply(ContainerFeeDetailEntity value, FeeItemEntity item, private static void apply(ContainerFeeDetailEntity value, FeeItemEntity item,
SaveRequest request, long operatorId) { 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.setFeeItemId(item.getId());
value.setFeeItemCodeSnapshot(item.getFeeCode()); value.setFeeItemCodeSnapshot(item.getFeeCode());
value.setFeeItemNameSnapshot(item.getFeeTypeName()); value.setFeeItemNameSnapshot(item.getFeeTypeName());
value.setDirection(request.direction()); 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.setBusinessDate(request.businessDate());
value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP)); value.setAmount(request.amount().setScale(2, RoundingMode.HALF_UP));
value.setCurrency(request.currency()); value.setCurrency(request.currency());
@@ -61,11 +61,13 @@ public class FinanceFeeSourceSnapshotService {
String canonical = String.join("|", "CONTAINER_FEE", String.valueOf(feeId), String canonical = String.join("|", "CONTAINER_FEE", String.valueOf(feeId),
String.valueOf(version), String.valueOf(detail.getTenantId()), String.valueOf(containerId), String.valueOf(version), String.valueOf(detail.getTenantId()), String.valueOf(containerId),
nullSafe(detail.getFeeItemCodeSnapshot()), nullSafe(detail.getDirection()), nullSafe(detail.getFeeItemCodeSnapshot()), nullSafe(detail.getDirection()),
nullSafe(detail.getPartnerId()), nullSafe(detail.getPartnerName()),
detail.getAmount().toPlainString(), nullSafe(detail.getCurrency()), detail.getAmount().toPlainString(), nullSafe(detail.getCurrency()),
String.valueOf(detail.getSettlementIncluded())); String.valueOf(detail.getSettlementIncluded()));
return new FinanceFeeSourceSnapshot(detail.getTenantId(), "CONTAINER_FEE", feeId, return new FinanceFeeSourceSnapshot(detail.getTenantId(), "CONTAINER_FEE", feeId,
version, null, null, null, containerId, detail.getContainerBookingId(), 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.getFeeItemCodeSnapshot(), detail.getFeeItemNameSnapshot(), detail.getDirection(),
detail.getBusinessDate(), detail.getCurrency(), detail.getAmount(), detail.getReviewStatus(), detail.getBusinessDate(), detail.getCurrency(), detail.getAmount(), detail.getReviewStatus(),
publicationStatus, Boolean.TRUE.equals(detail.getSettlementIncluded()), publicationStatus, Boolean.TRUE.equals(detail.getSettlementIncluded()),
@@ -37,8 +37,6 @@ CALL firstleg_finance_add_column('fl_inbound_outbox', 'locked_by',
CALL firstleg_finance_add_column('fl_inbound_outbox', 'last_error', CALL firstleg_finance_add_column('fl_inbound_outbox', 'last_error',
"VARCHAR(500) NULL COMMENT '最近一次投递失败摘要,不记录令牌' AFTER locked_by"); "VARCHAR(500) NULL COMMENT '最近一次投递失败摘要,不记录令牌' AFTER locked_by");
DROP PROCEDURE IF EXISTS firstleg_finance_add_column;
DROP PROCEDURE IF EXISTS firstleg_finance_add_index; DROP PROCEDURE IF EXISTS firstleg_finance_add_index;
DELIMITER $$ DELIMITER $$
CREATE PROCEDURE firstleg_finance_add_index( 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_code_snapshot VARCHAR(50) NOT NULL COMMENT '费用项目稳定编码快照',
fee_item_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用项目名称快照', fee_item_name_snapshot VARCHAR(128) NOT NULL COMMENT '费用项目名称快照',
direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付', 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 '费用业务日期', business_date DATE NOT NULL COMMENT '费用业务日期',
amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额', amount DECIMAL(18,2) NOT NULL COMMENT 'CNY费用金额',
currency CHAR(3) NOT NULL DEFAULT 'CNY' 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')) CONSTRAINT chk_container_fee_source CHECK (source_status IN ('ACTIVE', 'VOIDED'))
) ENGINE=InnoDB COMMENT='柜次级人工应收应付费用来源;审核通过且参与结算后同步Finance'; ) 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; DROP PROCEDURE IF EXISTS firstleg_finance_assert_schema;
DELIMITER $$ DELIMITER $$
CREATE PROCEDURE firstleg_finance_assert_schema() CREATE PROCEDURE firstleg_finance_assert_schema()
@@ -113,6 +119,11 @@ BEGIN
WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail') THEN WHERE table_schema = DATABASE() AND table_name = 'fl_container_fee_detail') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用表不存在'; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Firstleg Finance迁移失败:柜次费用表不存在';
END IF; 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$$ END$$
DELIMITER ; DELIMITER ;
CALL firstleg_finance_assert_schema(); CALL firstleg_finance_assert_schema();
@@ -7,6 +7,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.RecordComponent;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -39,6 +40,17 @@ class FeeManagementContractTest {
assertPermission("delete", "firstleg:fee-item:delete"); 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 @Test
@DisplayName("平台费用项目接口不接受客户作用域") @DisplayName("平台费用项目接口不接受客户作用域")
void platformFeeItemEndpointsDoNotExposeAccountId() { void platformFeeItemEndpointsDoNotExposeAccountId() {
@@ -16,7 +16,8 @@ class FinanceFeeSourceMigrationContractTest {
assertThat(sql).contains("information_schema.columns", "event_key", "payload_hash", assertThat(sql).contains("information_schema.columns", "event_key", "payload_hash",
"uk_inbound_outbox_event_key", "next_retry_at", "locked_at", "locked_by", "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", "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"); "chk_container_fee_currency", "currency = 'CNY'", "firstleg_finance_assert_schema");
} }
} }
@@ -85,6 +85,38 @@ class ContainerFeeDetailServiceTest {
verify(repository).adjust(captor.capture(), eq(1)); verify(repository).adjust(captor.capture(), eq(1));
assertThat(captor.getValue().getSourceVersion()).isEqualTo(2); assertThat(captor.getValue().getSourceVersion()).isEqualTo(2);
assertThat(captor.getValue().getReviewStatus()).isEqualTo("PENDING"); 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<ContainerFeeDetailEntity> captor = ArgumentCaptor.forClass(ContainerFeeDetailEntity.class);
verify(repository).insert(captor.capture());
assertThat(captor.getValue().getPartnerId()).isNull();
assertThat(captor.getValue().getPartnerName()).isNull();
} }
private void ownedPlan() { private void ownedPlan() {
@@ -101,6 +133,7 @@ class ContainerFeeDetailServiceTest {
value.setId(11L); value.setId(11L);
value.setFeeCode("TRUCKING"); value.setFeeCode("TRUCKING");
value.setFeeTypeName("拖车费"); value.setFeeTypeName("拖车费");
value.setWarehouseReceivable(true);
value.setWarehousePayable(true); value.setWarehousePayable(true);
value.setStatus("ACTIVE"); value.setStatus("ACTIVE");
return value; return value;
@@ -109,6 +142,6 @@ class ContainerFeeDetailServiceTest {
private static ContainerFeeDetailDtos.SaveRequest request(boolean settlementIncluded) { private static ContainerFeeDetailDtos.SaveRequest request(boolean settlementIncluded) {
return new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE", return new ContainerFeeDetailDtos.SaveRequest(11L, "PAYABLE",
LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY", LocalDate.of(2026, 8, 11), new BigDecimal("300.00"), "CNY",
settlementIncluded, "整柜拖车费"); settlementIncluded, 71L, " 现有往来单位 ", "整柜拖车费");
} }
} }
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException; import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot; import com.baiyue.firstleg.dto.FinanceFeeSourceSnapshot;
import com.baiyue.firstleg.entity.FeeItemEntity; import com.baiyue.firstleg.entity.FeeItemEntity;
import com.baiyue.firstleg.entity.ContainerFeeDetailEntity;
import com.baiyue.firstleg.entity.InboundFeeDetailEntity; import com.baiyue.firstleg.entity.InboundFeeDetailEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity; import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.ContainerFeeDetailRepository; import com.baiyue.firstleg.repository.ContainerFeeDetailRepository;
@@ -68,4 +69,34 @@ class FinanceFeeSourceSnapshotServiceTest {
assertThatThrownBy(() -> service.inbound(21L, 31L)) assertThatThrownBy(() -> service.inbound(21L, 31L))
.isInstanceOf(BizException.class).hasMessageContaining("不存在"); .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}");
}
} }