完善财务费用来源与柜级分摊
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import com.baiyue.common.event.BusinessFeeSourceChangedV1;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.finance.service.BusinessFeeSourceEventService;
|
||||
import com.baiyue.finance.service.FinanceSupplierValidator;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.startsWith;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class BusinessFeeSourceEventServiceTest {
|
||||
private final JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
private final ObjectMapper json = new ObjectMapper().findAndRegisterModules();
|
||||
private final FinanceSupplierValidator suppliers = mock(FinanceSupplierValidator.class);
|
||||
private final BusinessFeeSourceEventService service = new BusinessFeeSourceEventService(jdbc, json, suppliers);
|
||||
|
||||
@Test
|
||||
void importsOverseasFeeWithoutInventingInboundReference() {
|
||||
EventEnvelope event = eventWithoutInbound();
|
||||
when(jdbc.queryForList(startsWith("SELECT payload_hash,status,result_resource_id"),
|
||||
eq("LASTLEG"), eq(event.eventId()))).thenReturn(List.of());
|
||||
when(jdbc.queryForList(startsWith("SELECT id,source_hash,status FROM fin_fee_source"),
|
||||
eq("LASTLEG"), eq("OVERSEAS_PROCUREMENT_FEE"), eq(701L), eq(1))).thenReturn(List.of());
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_fee_source f WHERE f.source_system=?"),
|
||||
eq(Integer.class), eq("LASTLEG"), eq("OVERSEAS_PROCUREMENT_FEE"), eq(701L))).thenReturn(0);
|
||||
when(jdbc.queryForObject(startsWith("SELECT id FROM fin_fee_source"), eq(Long.class),
|
||||
eq("LASTLEG"), eq("OVERSEAS_PROCUREMENT_FEE"), eq(701L), eq(1))).thenReturn(901L);
|
||||
|
||||
assertThat(service.consume(event)).isEqualTo(901L);
|
||||
|
||||
ArgumentCaptor<Object[]> values = ArgumentCaptor.forClass(Object[].class);
|
||||
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture());
|
||||
assertThat(values.getValue()[3]).isEqualTo(10001L);
|
||||
assertThat(values.getValue()[5]).isEqualTo("PO-US-001");
|
||||
assertThat(values.getValue()[9]).isNull();
|
||||
assertThat(values.getValue()[13]).isNull();
|
||||
}
|
||||
|
||||
private EventEnvelope eventWithoutInbound() {
|
||||
BusinessFeeSourceChangedV1 unsigned = new BusinessFeeSourceChangedV1(null, "LASTLEG",
|
||||
"OVERSEAS_PROCUREMENT_FEE", "701", 1, "10001", "客户A", "PO-US-001",
|
||||
"OVERSEAS_PROCUREMENT", "801", "PO-US-001", null, null, null, null, null,
|
||||
null, "91", "PROCUREMENT_SERVICE", "采购服务费", "RECEIVABLE", null, null,
|
||||
null, null, null, null, null, List.of(), LocalDate.of(2026, 9, 2), "CNY",
|
||||
new BigDecimal("120.00"), "APPROVED", "PUBLISHED", "ACTIVE", null);
|
||||
String sourceHash = EventPayloadHash.sha256(
|
||||
EventPayloadCanonicalizer.canonicalize(json.valueToTree(unsigned)));
|
||||
BusinessFeeSourceChangedV1 payload = new BusinessFeeSourceChangedV1("PUBLISHED", "LASTLEG",
|
||||
"OVERSEAS_PROCUREMENT_FEE", "701", 1, "10001", "客户A", "PO-US-001",
|
||||
"OVERSEAS_PROCUREMENT", "801", "PO-US-001", null, null, null, null, null,
|
||||
null, "91", "PROCUREMENT_SERVICE", "采购服务费", "RECEIVABLE", null, null,
|
||||
null, null, null, null, null, List.of(), LocalDate.of(2026, 9, 2), "CNY",
|
||||
new BigDecimal("120.00"), "APPROVED", "PUBLISHED", "ACTIVE", sourceHash);
|
||||
JsonNode node = json.valueToTree(payload);
|
||||
String payloadHash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node));
|
||||
return new EventEnvelope("123e4567-e89b-12d3-a456-426614174701",
|
||||
EventTypes.BUSINESS_FEE_SOURCE_CHANGED, 1, Instant.parse("2026-09-02T08:00:00Z"),
|
||||
RegionCode.OVERSEAS, RegionCode.CN, "10001", "BUSINESS_FEE", "701", 1,
|
||||
null, "123e4567-e89b-12d3-a456-426614174702", "LASTLEG:FEE:701:1", null,
|
||||
node, payloadHash);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ class CustomerAccountSummaryBehaviorTest {
|
||||
.contains("FROM fin_settlement s WHERE s.tenant_id=?")
|
||||
.contains("s.direction='RECEIVABLE' AND s.status<>'VOIDED'")
|
||||
.contains("os.direction='PAYABLE' AND os.status<>'VOIDED'")
|
||||
.contains("f.settlement_mode='ON_ACCOUNT'", "c.cost_origin='FIRSTLEG_DIRECT_PAID'", "ps.paid_amount")
|
||||
.contains("f.settlement_mode='ON_ACCOUNT'", "c.cost_origin IN ('FIRSTLEG_DIRECT_PAID','BUSINESS_DIRECT_PAID')", "ps.paid_amount")
|
||||
.contains("f.customer_id=?", "f.status='AVAILABLE'", "f.review_status='APPROVED'", "f.amount>0")
|
||||
.contains("b.status<>'VOIDED'", "FROM fin_settlement_line sl", "sl.fee_source_id=f.id", "aps.status<>'VOIDED'")
|
||||
.doesNotContain("JSON_EXTRACT")
|
||||
|
||||
@@ -59,7 +59,7 @@ class FinanceCustomerAccountServiceTest {
|
||||
String all = String.join("\n", sql.getAllValues());
|
||||
assertThat(all)
|
||||
.contains("SUM(outstanding_amount) amount FROM fin_settlement", "direction='RECEIVABLE'")
|
||||
.contains("cost_origin='FIRSTLEG_DIRECT_PAID' AND status='CONFIRMED'")
|
||||
.contains("cost_origin IN ('FIRSTLEG_DIRECT_PAID','BUSINESS_DIRECT_PAID') AND status='CONFIRMED'")
|
||||
.contains("direction='PAYABLE' AND status<>'VOIDED'")
|
||||
.contains("NOT EXISTS (SELECT 1 FROM fin_settlement_line")
|
||||
.contains("sl.fee_source_id=f.id", "s.status<>'VOIDED'")
|
||||
|
||||
@@ -61,7 +61,7 @@ class FinanceFeeSourceEventServiceTest {
|
||||
|
||||
assertThat(service.consume(event)).isEqualTo(101L);
|
||||
verify(firstleg).container("test-token", 41, 31, 1);
|
||||
verify(jdbc).update(contains("status='SUPERSEDED'"), eq(31L));
|
||||
verify(jdbc).update(contains("status='SUPERSEDED'"), eq("CONTAINER_FEE"), eq(31L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -143,7 +143,7 @@ class FinanceFeeSourceEventServiceTest {
|
||||
when(firstleg.inbound(anyString(), eq(21L), eq(31L), eq(2))).thenReturn(ApiResponse.success(snapshot("INBOUND_FEE", 7, 2, 21L, null)));
|
||||
|
||||
assertThat(service.consume(event)).isEqualTo(101L);
|
||||
verify(jdbc).update(contains("status='SUPERSEDED'"), eq(31L));
|
||||
verify(jdbc).update(contains("status='SUPERSEDED'"), eq("INBOUND_FEE"), eq(31L));
|
||||
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), any(Object[].class));
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ class FinanceFeeSourceEventServiceTest {
|
||||
when(firstleg.inbound(anyString(), eq(21L), eq(31L), eq(2)))
|
||||
.thenReturn(ApiResponse.success(snapshot("INBOUND_FEE", 7, 2, 21L, null)));
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_fee_source f WHERE f.source_system='FIRSTLEG'"),
|
||||
eq(Integer.class), eq(31L))).thenReturn(1);
|
||||
eq(Integer.class), anyString(), eq(31L))).thenReturn(1);
|
||||
|
||||
assertThatThrownBy(() -> service.consume(event))
|
||||
.isInstanceOf(FinanceEventException.class)
|
||||
@@ -168,7 +168,7 @@ class FinanceFeeSourceEventServiceTest {
|
||||
FinanceFeeSourceEvent event = inboundEvent("voided-2", "FIRSTLEG_FEE_VOIDED", 2, 7);
|
||||
when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("FIRSTLEG"), eq("voided-2"))).thenReturn(List.of());
|
||||
when(firstleg.inbound(anyString(), eq(21L), eq(31L), eq(2))).thenReturn(ApiResponse.success(snapshot("INBOUND_FEE", 7, 2, 21L, null)));
|
||||
when(jdbc.queryForList(startsWith("SELECT id,source_hash,status"), eq(31L), eq(2)))
|
||||
when(jdbc.queryForList(startsWith("SELECT id,source_hash,status"), anyString(), eq(31L), eq(2)))
|
||||
.thenReturn(List.of(Map.of("id", 88L, "source_hash", "source-hash", "status", "AVAILABLE")));
|
||||
|
||||
assertThat(service.consume(event)).isEqualTo(88L);
|
||||
@@ -183,7 +183,7 @@ class FinanceFeeSourceEventServiceTest {
|
||||
.thenReturn(List.of());
|
||||
when(firstleg.inbound(anyString(), eq(21L), eq(31L), eq(2)))
|
||||
.thenReturn(ApiResponse.success(snapshot("INBOUND_FEE", 7, 2, 21L, null)));
|
||||
when(jdbc.queryForList(startsWith("SELECT id,source_hash,status"), eq(31L), eq(2)))
|
||||
when(jdbc.queryForList(startsWith("SELECT id,source_hash,status"), anyString(), eq(31L), eq(2)))
|
||||
.thenReturn(List.of(Map.of("id", 88L, "source_hash", "source-hash", "status", "AVAILABLE")));
|
||||
when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_fee_source f WHERE f.id=?"),
|
||||
eq(Integer.class), eq(88L))).thenReturn(1);
|
||||
@@ -253,8 +253,8 @@ class FinanceFeeSourceEventServiceTest {
|
||||
|
||||
private void givenNewEvent() {
|
||||
when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("FIRSTLEG"), anyString())).thenReturn(List.of());
|
||||
when(jdbc.queryForList(startsWith("SELECT id,source_hash,status"), eq(31L), anyInt())).thenReturn(List.of());
|
||||
when(jdbc.queryForObject(startsWith("SELECT id FROM fin_fee_source"), eq(Long.class), eq(31L), anyInt())).thenReturn(101L);
|
||||
when(jdbc.queryForList(startsWith("SELECT id,source_hash,status"), anyString(), eq(31L), anyInt())).thenReturn(List.of());
|
||||
when(jdbc.queryForObject(startsWith("SELECT id FROM fin_fee_source"), eq(Long.class), anyString(), eq(31L), anyInt())).thenReturn(101L);
|
||||
}
|
||||
|
||||
private FinanceFeeSourceEvent inboundEvent(String id, String type, int version, long tenant) {
|
||||
|
||||
@@ -16,7 +16,7 @@ class FinanceFeeSourceMigrationContractTest {
|
||||
"source_hash", "source_snapshot", "payload_hash", "result_resource_id", "error_code");
|
||||
assertThat(sql).contains("settlement_mode", "paid_at", "payment_method",
|
||||
"payment_reference_no", "payment_voucher_file_ids");
|
||||
assertThat(sql).contains("cost_origin", "FIRSTLEG_DIRECT_PAID", "source_fee_version",
|
||||
assertThat(sql).contains("cost_origin", "FIRSTLEG_DIRECT_PAID", "BUSINESS_DIRECT_PAID", "warehouse_id", "source_fee_version",
|
||||
"uk_fin_cost_source_version", "Finance直接已付成本字段不完整");
|
||||
assertThat(sql).contains("AVAILABLE可结算", "SUPERSEDED已被新版本替代", "VOIDED已作废");
|
||||
assertThat(sql).contains("finance_fee_source_assert_schema");
|
||||
|
||||
@@ -107,7 +107,7 @@ class FinancePhase1OperationBehaviorTest {
|
||||
assertThat(result.traceId()).isNotBlank();
|
||||
assertThat(jdbc.accountListSql).contains("p.customer_name_snapshot");
|
||||
assertThat(jdbc.accountListSql)
|
||||
.contains("ar.direction='RECEIVABLE'", "cost_origin='FIRSTLEG_DIRECT_PAID'", "FROM fin_settlement_line sl")
|
||||
.contains("ar.direction='RECEIVABLE'", "cost_origin IN ('FIRSTLEG_DIRECT_PAID','BUSINESS_DIRECT_PAID')", "FROM fin_settlement_line sl")
|
||||
.contains("sl.fee_source_id=f.id", "s.status<>'VOIDED'");
|
||||
assertThat(jdbc.accountListSql).doesNotContain("CAST(c.customer_id AS CHAR)");
|
||||
}
|
||||
@@ -123,7 +123,7 @@ class FinancePhase1OperationBehaviorTest {
|
||||
assertThat(result.data()).isEmpty();
|
||||
assertThat(jdbc.ledgerCustomerIds).containsExactly(9L, 9L, 9L, 9L);
|
||||
assertThat(jdbc.ledgerSql).anySatisfy(sql -> assertThat(sql)
|
||||
.contains("cost_origin='FIRSTLEG_DIRECT_PAID'", "status='CONFIRMED'"));
|
||||
.contains("cost_origin IN ('FIRSTLEG_DIRECT_PAID','BUSINESS_DIRECT_PAID')", "status='CONFIRMED'"));
|
||||
assertThat(jdbc.ledgerSql).anySatisfy(sql -> assertThat(sql)
|
||||
.contains("fin_settlement_payment", "transaction_type='PAYMENT'", "s.direction='PAYABLE'"));
|
||||
}
|
||||
@@ -240,9 +240,9 @@ class FinancePhase1OperationBehaviorTest {
|
||||
var request = new FinancePhase1Dtos.FeeSourceEditRequest(
|
||||
new BigDecimal("120.00"), "更正金额", 1);
|
||||
assertThatThrownBy(() -> service.editFinanceFeeSource(88L, 71L, "edit-key", request, false))
|
||||
.isInstanceOf(FinanceEventException.class).hasMessageContaining("Firstleg费用必须");
|
||||
.isInstanceOf(FinanceEventException.class).hasMessageContaining("业务费用必须回到对应业务费用明细调整");
|
||||
assertThatThrownBy(() -> service.editFinanceFeeSource(88L, 71L, "adjust-key", request, true))
|
||||
.isInstanceOf(FinanceEventException.class).hasMessageContaining("Firstleg费用必须");
|
||||
.isInstanceOf(FinanceEventException.class).hasMessageContaining("业务费用必须回到对应业务费用明细调整");
|
||||
verify(jdbc, never()).update(startsWith("UPDATE fin_fee_source SET amount="),
|
||||
(Object[]) any());
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ class FinanceSqlCommentContractTest {
|
||||
}
|
||||
}
|
||||
assertThat(tableCount).isEqualTo(17);
|
||||
assertThat(columnCount).isEqualTo(257);
|
||||
assertThat(columnCount).isEqualTo(265);
|
||||
assertThat(missing).as("缺少中文业务注释的Finance字段").isEmpty();
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class FinanceSqlCommentContractTest {
|
||||
assertThat(sql).contains("RECEIVED已接收、PROCESSED已完成、FAILED处理失败");
|
||||
assertThat(sql).contains("Firstleg费用来源完整不可变JSON快照");
|
||||
assertThat(sql).contains("ON_ACCOUNT账期应付、DIRECT_PAID业务已付");
|
||||
assertThat(sql).contains("仅FIRSTLEG_DIRECT_PAID且CONFIRMED计入已线下支付供应商款");
|
||||
assertThat(sql).contains("来源直接已付且CONFIRMED计入已线下支付供应商款");
|
||||
assertThat(sql).contains("RECEIVABLE计待收,PAYABLE与未制单ON_ACCOUNT来源共同计待付");
|
||||
assertThat(sql).contains("规范化完整请求的SHA-256十六进制哈希");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user