From 5b53be23e86362d849b15deee0989ed3be3914c5 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 26 Oct 2026 16:45:36 +0800 Subject: [PATCH] fix(finance): show direct-paid costs as fully paid --- .../finance/service/FinanceQueryService.java | 7 +- .../FinanceDirectPaidCostReadTest.java | 89 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 finance/src/test/java/com/baiyue/finance/FinanceDirectPaidCostReadTest.java diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceQueryService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceQueryService.java index 691d46130..9d41b2eb9 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinanceQueryService.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceQueryService.java @@ -219,6 +219,7 @@ public class FinanceQueryService { row.outstandingAmount(), row.currency(), row.status(), row.version())).toList(); } + /** 成本查询按来源付款事实回显金额,直接已付不能再次展示为待付款。 */ public List payableSettlements(Long customerId) { validateOptionalCustomer(customerId); String sql = "SELECT id,tenant_id,cost_no,cost_type,order_id,inbound_id,container_id,supplier_name_snapshot,fee_item_name,cost_amount,currency_code,status,created_at,version FROM fin_cost WHERE 1=1"; @@ -227,9 +228,13 @@ public class FinanceQueryService { return jdbc.query(sql + " ORDER BY created_at DESC,id DESC", (rs, rowNum) -> { BigDecimal amount = rs.getBigDecimal("cost_amount"); + boolean directPaid = "SOURCE_DIRECT_PAID".equals(rs.getString("cost_type")); + // 已确认直付成本已全额付款;作废历史不再计入有效已付,也不形成新应付。 + BigDecimal paid = directPaid && "CONFIRMED".equals(rs.getString("status")) ? amount : BigDecimal.ZERO; + BigDecimal outstanding = directPaid ? BigDecimal.ZERO : amount; return new PayableSettlement(rs.getLong("id"), rs.getLong("tenant_id"), rs.getString("cost_no"), rs.getString("cost_type"), nullableLong(rs, "order_id"), nullableLong(rs, "inbound_id"), nullableLong(rs, "container_id"), rs.getString("supplier_name_snapshot"), - rs.getString("fee_item_name"), amount, BigDecimal.ZERO, amount, rs.getString("currency_code"), rs.getString("status"), + rs.getString("fee_item_name"), amount, paid, outstanding, rs.getString("currency_code"), rs.getString("status"), time(rs, "created_at"), rs.getInt("version")); }, args); } diff --git a/finance/src/test/java/com/baiyue/finance/FinanceDirectPaidCostReadTest.java b/finance/src/test/java/com/baiyue/finance/FinanceDirectPaidCostReadTest.java new file mode 100644 index 000000000..11c995fd1 --- /dev/null +++ b/finance/src/test/java/com/baiyue/finance/FinanceDirectPaidCostReadTest.java @@ -0,0 +1,89 @@ +package com.baiyue.finance; + +import com.baiyue.finance.service.FinanceQueryService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** 成本列表和详情须与直接已付事实一致,不能提示供应商费用仍待支付。 */ +class FinanceDirectPaidCostReadTest { + private JdbcTemplate jdbc; + private FinanceQueryService service; + + /** 使用隔离内存表验证正式读取路径,不访问共享测试环境。 */ + @BeforeEach + void setUp() { + jdbc = new JdbcTemplate(new DriverManagerDataSource( + "jdbc:h2:mem:cost-read-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "")); + jdbc.execute("CREATE TABLE fin_cost(id BIGINT PRIMARY KEY,tenant_id BIGINT,cost_no VARCHAR(128)," + + "cost_type VARCHAR(24),cost_origin VARCHAR(24),order_id BIGINT,inbound_id BIGINT,container_id BIGINT," + + "supplier_name_snapshot VARCHAR(128),fee_item_name VARCHAR(128),cost_amount DECIMAL(18,2)," + + "currency_code VARCHAR(3),status VARCHAR(24),created_at TIMESTAMP,version INT)"); + jdbc.execute("CREATE TABLE fin_cost_allocation(id BIGINT,tenant_id BIGINT,cost_id BIGINT,order_id BIGINT," + + "inbound_id BIGINT,volume_cbm DECIMAL(18,6),total_volume_cbm DECIMAL(18,6),allocation_rate DECIMAL(18,6)," + + "allocated_amount DECIMAL(18,2),rounding_adjustment BOOLEAN)"); + service = new FinanceQueryService(jdbc, new ObjectMapper()); + } + + /** 头程与海外直付均已付款,列表和详情不能再次显示未付余额。 */ + @ParameterizedTest + @ValueSource(strings = {"BUSINESS_DIRECT_PAID", "FIRSTLEG_DIRECT_PAID"}) + void confirmedDirectPaidCostIsFullyPaidInListAndDetail(String origin) { + insertCost(1, 11, "SOURCE_DIRECT_PAID", origin, "CONFIRMED"); + + var row = service.payableSettlements(11L).get(0); + assertThat(row.payableAmount()).isEqualByComparingTo("0.03"); + assertThat(row.paidAmount()).isEqualByComparingTo("0.03"); + assertThat(row.outstandingAmount()).isZero(); + assertThat(service.cost(11, 1).cost()).isEqualTo(row); + } + + /** 已作废的直付历史保留原金额,但不再作为有效已付或待付款展示。 */ + @Test + void voidedDirectPaidCostHasNoActivePaymentOrOutstandingAmount() { + insertCost(1, 11, "SOURCE_DIRECT_PAID", "BUSINESS_DIRECT_PAID", "VOIDED"); + + var row = service.payableSettlements(11L).get(0); + assertThat(row.payableAmount()).isEqualByComparingTo("0.03"); + assertThat(row.paidAmount()).isZero(); + assertThat(row.outstandingAmount()).isZero(); + assertThat(row.status()).isEqualTo("VOIDED"); + } + + /** 人工补充成本没有直接已付事实,不能被自动视为付款完成。 */ + @ParameterizedTest + @ValueSource(strings = {"SINGLE_ORDER", "CONTAINER_SHARED"}) + void manualCostKeepsItsExistingUnpaidAmounts(String type) { + insertCost(1, 11, type, "MANUAL", "CONFIRMED"); + + var row = service.payableSettlements(11L).get(0); + assertThat(row.paidAmount()).isZero(); + assertThat(row.outstandingAmount()).isEqualByComparingTo("0.03"); + } + + /** 金额回显修复不能让其他客户读到成本列表或详情。 */ + @Test + void costReadsKeepCustomerIsolation() { + insertCost(1, 11, "SOURCE_DIRECT_PAID", "BUSINESS_DIRECT_PAID", "CONFIRMED"); + + assertThat(service.payableSettlements(12L)).isEmpty(); + assertThatThrownBy(() -> service.cost(12, 1)).hasMessageContaining("成本不存在或不属于客户"); + } + + /** 仅构造测试所需的成本事实,金额对应本轮发现问题的小额样本。 */ + private void insertCost(long id, long customer, String type, String origin, String status) { + jdbc.update("INSERT INTO fin_cost(id,tenant_id,cost_no,cost_type,cost_origin,order_id,container_id," + + "supplier_name_snapshot,fee_item_name,cost_amount,currency_code,status,created_at,version) " + + "VALUES(?,?,?,?,?,101,201,'验收供应商','人工费',0.03,'CNY',?,CURRENT_TIMESTAMP,0)", + id, customer, "COST-READ-TEST", type, origin, status); + } +}