优化
This commit is contained in:
@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@FeignClient(name = "firstleg-service", contextId = "financeFirstlegFeeSourceClient",
|
||||
url = "${baiyue.clients.firstleg.url:}", path = "/internal/firstleg/v1")
|
||||
@@ -45,6 +46,8 @@ public interface FirstlegFeeSourceClient {
|
||||
Long containerBookingId, String containerNo, long customerId,
|
||||
String customerName, Long supplierId, String supplierName,
|
||||
String supplierCategory,
|
||||
String settlementMode, LocalDateTime paidAt, String paymentMethod,
|
||||
String paymentReferenceNo, List<Long> paymentVoucherFileIds,
|
||||
Long feeItemId, String feeItemCode,
|
||||
String feeItemName, String direction, LocalDate businessDate,
|
||||
String currency, BigDecimal totalAmount, String reviewStatus,
|
||||
|
||||
@@ -53,6 +53,11 @@ public final class FinanceReadModels {
|
||||
String feeItemName,
|
||||
BigDecimal amount,
|
||||
String currency,
|
||||
String settlementMode,
|
||||
LocalDateTime paidAt,
|
||||
String paymentMethod,
|
||||
String paymentReferenceNo,
|
||||
List<String> paymentVoucherFileIds,
|
||||
String status,
|
||||
LocalDateTime createdAt) {}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ public class FinanceCustomerAccountService {
|
||||
.forEach(row -> metric(result, row, "pendingBilling", "amount"));
|
||||
jdbc.queryForList("SELECT tenant_id customer_id,SUM(received_amount) amount FROM fin_receipt WHERE tenant_id IN (" + placeholders + ") AND status='CONFIRMED' GROUP BY tenant_id", customerIds.toArray())
|
||||
.forEach(row -> metric(result, row, "receivedInPeriod", "amount"));
|
||||
jdbc.queryForList("SELECT tenant_id customer_id,SUM(cost_amount) amount FROM fin_cost WHERE tenant_id IN (" + placeholders + ") AND status<>'VOIDED' GROUP BY tenant_id", customerIds.toArray())
|
||||
jdbc.queryForList("SELECT tenant_id customer_id,SUM(outstanding_amount) amount FROM fin_settlement WHERE tenant_id IN (" + placeholders + ") AND direction='PAYABLE' AND status<>'VOIDED' GROUP BY tenant_id", customerIds.toArray())
|
||||
.forEach(row -> metric(result, row, "outstandingPayable", "amount"));
|
||||
Object[] changeArgs = new Object[customerIds.size() * 3];
|
||||
for (int i = 0; i < 3; i++) System.arraycopy(customerIds.toArray(), 0, changeArgs, i * customerIds.size(), customerIds.size());
|
||||
|
||||
@@ -81,11 +81,13 @@ public class FinanceFeeSourceEventService {
|
||||
}
|
||||
jdbc.update("UPDATE fin_fee_source SET status='SUPERSEDED' WHERE source_system='FIRSTLEG' AND source_fee_id=? AND status='AVAILABLE'",
|
||||
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,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,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.sourceType().equals("INBOUND_FEE") ? snapshot.inboundCode() : snapshot.containerNo(),
|
||||
snapshot.direction(), snapshot.orderId(), snapshot.inboundId(), snapshot.containerId(),
|
||||
snapshot.feeItemCode(), snapshot.feeItemName(), snapshot.totalAmount(), snapshot.currency(),
|
||||
snapshot.settlementMode(), snapshot.paidAt(), snapshot.paymentMethod(),
|
||||
snapshot.paymentReferenceNo(), voucherJson(snapshot.paymentVoucherFileIds()),
|
||||
snapshot.sourceHash(), json(snapshot));
|
||||
return jdbc.queryForObject("SELECT id FROM fin_fee_source WHERE source_system='FIRSTLEG' AND source_fee_id=? AND source_fee_version=?",
|
||||
Long.class, snapshot.feeId(), snapshot.feeVersion());
|
||||
@@ -138,14 +140,44 @@ public class FinanceFeeSourceEventService {
|
||||
if (!value.supplierName().equals(supplier.name())) {
|
||||
throw error("FIN-EVT-012", "应付费用来源供应商名称快照不一致");
|
||||
}
|
||||
validatePayableSettlement(value);
|
||||
} else if (value.supplierId() != null || !blank(value.supplierName())
|
||||
|| !blank(value.supplierCategory())) {
|
||||
throw error("FIN-EVT-012", "应收费用不得携带供应商快照");
|
||||
} else if (!blank(value.settlementMode()) || value.paidAt() != null
|
||||
|| !blank(value.paymentMethod()) || !blank(value.paymentReferenceNo())
|
||||
|| (value.paymentVoucherFileIds() != null && !value.paymentVoucherFileIds().isEmpty())) {
|
||||
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", "柜子归属不一致");
|
||||
}
|
||||
|
||||
private void validatePayableSettlement(FirstlegFeeSourceClient.Snapshot value) {
|
||||
boolean hasVouchers = value.paymentVoucherFileIds() != null
|
||||
&& !value.paymentVoucherFileIds().isEmpty();
|
||||
if (hasVouchers && (value.paymentVoucherFileIds().size() > 20
|
||||
|| value.paymentVoucherFileIds().stream().anyMatch(id -> id == null || id <= 0))) {
|
||||
throw error("FIN-EVT-012", "付款凭证文件快照无效");
|
||||
}
|
||||
if ("ON_ACCOUNT".equals(value.settlementMode())) {
|
||||
if (value.paidAt() != null || !blank(value.paymentMethod())
|
||||
|| !blank(value.paymentReferenceNo()) || hasVouchers) {
|
||||
throw error("FIN-EVT-012", "账期应付不得携带已付快照");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!"DIRECT_PAID".equals(value.settlementMode()) || value.paidAt() == null
|
||||
|| !Set.of("ONLINE", "BANK_TRANSFER", "CASH", "OTHER").contains(value.paymentMethod())
|
||||
|| (blank(value.paymentReferenceNo()) && !hasVouchers)) {
|
||||
throw error("FIN-EVT-012", "直接已付费用来源快照不完整");
|
||||
}
|
||||
}
|
||||
|
||||
private String voucherJson(java.util.List<Long> values) {
|
||||
return values == null || values.isEmpty() ? null : json(values);
|
||||
}
|
||||
|
||||
private String json(Object value) {
|
||||
try { return objectMapper.writeValueAsString(value); }
|
||||
catch (JsonProcessingException e) { throw error("FIN-EVT-001", "事件序列化失败"); }
|
||||
|
||||
@@ -69,7 +69,7 @@ public class FinancePhase1OperationService {
|
||||
"(SELECT COALESCE(SUM(outstanding_amount),0) FROM fin_bill b WHERE b.tenant_id=c.customer_id AND b.status<>'VOIDED') outstanding_receivable," +
|
||||
"(SELECT COALESCE(SUM(amount),0) FROM fin_fee_source f WHERE f.customer_id=c.customer_id AND f.direction='RECEIVABLE' AND f.status='AVAILABLE' AND NOT EXISTS(SELECT 1 FROM fin_bill_line bl WHERE bl.source_id=f.id AND bl.source_version=f.source_fee_version)) pending_billing," +
|
||||
"(SELECT COALESCE(SUM(received_amount),0) FROM fin_receipt r WHERE r.tenant_id=c.customer_id AND r.status='CONFIRMED') received_period," +
|
||||
"(SELECT COALESCE(SUM(cost_amount),0) FROM fin_cost x WHERE x.tenant_id=c.customer_id AND x.status<>'VOIDED') outstanding_payable," +
|
||||
"(SELECT COALESCE(SUM(outstanding_amount),0) FROM fin_settlement x WHERE x.tenant_id=c.customer_id AND x.direction='PAYABLE' AND x.status<>'VOIDED') outstanding_payable," +
|
||||
"GREATEST(COALESCE((SELECT MAX(updated_at) FROM fin_bill b WHERE b.tenant_id=c.customer_id),'1970-01-01'),COALESCE((SELECT MAX(updated_at) FROM fin_receipt r WHERE r.tenant_id=c.customer_id),'1970-01-01'),COALESCE((SELECT MAX(updated_at) FROM fin_cost x WHERE x.tenant_id=c.customer_id),'1970-01-01')) last_changed_at" + base + " ORDER BY last_changed_at DESC,c.customer_id DESC LIMIT ? OFFSET ?";
|
||||
Object[] args = java.util.Arrays.copyOf(filterArgs, filterArgs.length + 2);
|
||||
args[args.length - 2] = pageSize; args[args.length - 1] = (page - 1) * pageSize;
|
||||
|
||||
@@ -96,7 +96,7 @@ public class FinanceQueryService {
|
||||
String normalized = direction == null || direction.isBlank() ? "ALL" : direction.trim().toUpperCase(java.util.Locale.ROOT);
|
||||
if (!List.of("ALL", "RECEIVABLE", "PAYABLE").contains(normalized)) throw bad("费用方向只能是RECEIVABLE或PAYABLE");
|
||||
List<FeePoolItem> result = new ArrayList<>();
|
||||
String sourceSql = "SELECT id,customer_id,direction,source_type,source_fee_id,source_fee_version,order_id,inbound_id,container_id,fee_item_name,amount,currency_code,status,created_at FROM fin_fee_source WHERE status='AVAILABLE'";
|
||||
String sourceSql = "SELECT id,customer_id,direction,source_type,source_fee_id,source_fee_version,order_id,inbound_id,container_id,fee_item_name,amount,currency_code,settlement_mode,paid_at,payment_method,payment_reference_no,payment_voucher_file_ids,status,created_at FROM fin_fee_source WHERE status='AVAILABLE'";
|
||||
List<Object> sourceArgs = new ArrayList<>();
|
||||
if (customerId != null) { sourceSql += " AND customer_id=?"; sourceArgs.add(customerId); }
|
||||
if (!"ALL".equals(normalized)) { sourceSql += " AND direction=?"; sourceArgs.add(normalized); }
|
||||
@@ -107,18 +107,10 @@ public class FinanceQueryService {
|
||||
rs.getString("source_type"), rs.getLong("source_fee_id"), rs.getInt("source_fee_version"),
|
||||
nullableLong(rs, "order_id"), nullableLong(rs, "inbound_id"), nullableLong(rs, "container_id"),
|
||||
rs.getString("fee_item_name"), rs.getBigDecimal("amount"), rs.getString("currency_code"),
|
||||
rs.getString("settlement_mode"), time(rs, "paid_at"), rs.getString("payment_method"),
|
||||
rs.getString("payment_reference_no"), fileIds(rs.getString("payment_voucher_file_ids")),
|
||||
rs.getString("status"), time(rs, "created_at"));
|
||||
}, sourceArgs.toArray()));
|
||||
if (!"RECEIVABLE".equals(normalized)) {
|
||||
String costSql = "SELECT id,tenant_id,order_id,inbound_id,container_id,fee_item_name,cost_amount,currency_code,status,created_at FROM fin_cost";
|
||||
Object[] costArgs = new Object[0];
|
||||
if (customerId != null) { costSql += " WHERE tenant_id=?"; costArgs = new Object[]{customerId}; }
|
||||
costSql += " ORDER BY created_at DESC,id DESC";
|
||||
result.addAll(jdbc.query(costSql, (rs, rowNum) ->
|
||||
new FeePoolItem("PAYABLE:" + rs.getLong("id"), rs.getLong("tenant_id"), "PAYABLE", "MANUAL_COST", rs.getLong("id"), 1,
|
||||
nullableLong(rs, "order_id"), nullableLong(rs, "inbound_id"), nullableLong(rs, "container_id"),
|
||||
rs.getString("fee_item_name"), rs.getBigDecimal("cost_amount"), rs.getString("currency_code"), rs.getString("status"), time(rs, "created_at")), costArgs));
|
||||
}
|
||||
result.sort(Comparator.comparing(FeePoolItem::createdAt, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ public class FinanceSettlementService {
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.addAll(ids);
|
||||
List<Map<String, Object>> sources = jdbc.queryForList(
|
||||
"SELECT id,customer_id,direction,currency_code,status,amount,fee_item_code,fee_item_name,source_snapshot," +
|
||||
"SELECT id,customer_id,direction,currency_code,status,amount,fee_item_code,fee_item_name,settlement_mode,source_snapshot," +
|
||||
"review_status," +
|
||||
"CAST(JSON_UNQUOTE(JSON_EXTRACT(source_snapshot,'$.supplierId')) AS UNSIGNED) supplier_id," +
|
||||
"JSON_UNQUOTE(JSON_EXTRACT(source_snapshot,'$.supplierName')) supplier_name," +
|
||||
@@ -152,6 +152,9 @@ public class FinanceSettlementService {
|
||||
for (Map<String, Object> source : sources) {
|
||||
Long supplierId = nullableNumber(source, "supplier_id");
|
||||
if (supplierId == null || supplierId != request.supplierId()) throw error("FIN_409", "应付来源必须属于同一供应商");
|
||||
if (!"ON_ACCOUNT".equals(text(source, "settlement_mode"))) {
|
||||
throw error("FIN_409", "只有ON_ACCOUNT账期应付费用可以生成应付结算单");
|
||||
}
|
||||
if (!"PAYABLE".equals(text(source, "direction")) || !"AVAILABLE".equals(text(source, "status"))
|
||||
|| !"APPROVED".equals(text(source, "review_status")) || decimal(source, "amount").signum() <= 0) {
|
||||
throw error("FIN_409", "费用来源未审核、金额无效或状态不可用");
|
||||
|
||||
@@ -31,6 +31,16 @@ CALL finance_fee_source_add_column('fin_fee_source', 'fee_item_code',
|
||||
"VARCHAR(64) NULL COMMENT '来源费用项目编码快照' AFTER container_id");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'source_hash',
|
||||
"CHAR(64) NULL COMMENT 'Firstleg费用来源完整快照SHA-256哈希' AFTER currency_code");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'settlement_mode',
|
||||
"VARCHAR(16) NULL COMMENT '仅PAYABLE:ON_ACCOUNT账期应付、DIRECT_PAID业务已付;RECEIVABLE为空' AFTER currency_code");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'paid_at',
|
||||
"DATETIME(3) NULL COMMENT 'DIRECT_PAID业务已付时间' AFTER settlement_mode");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'payment_method',
|
||||
"VARCHAR(32) NULL COMMENT 'DIRECT_PAID付款方式' AFTER paid_at");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'payment_reference_no',
|
||||
"VARCHAR(128) NULL COMMENT 'DIRECT_PAID付款流水号' AFTER payment_method");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'payment_voucher_file_ids',
|
||||
"JSON NULL COMMENT 'DIRECT_PAID付款凭证File ID数组' AFTER payment_reference_no");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'source_snapshot',
|
||||
"JSON NULL COMMENT 'Firstleg费用来源完整不可变JSON快照' AFTER source_hash");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'review_status',
|
||||
@@ -90,8 +100,9 @@ BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fin_fee_source'
|
||||
AND column_name IN ('source_type','source_no','direction','container_id','fee_item_code',
|
||||
'source_hash','source_snapshot','review_status','review_reason',
|
||||
'operation_version','updated_at')) <> 11 THEN
|
||||
'settlement_mode','paid_at','payment_method','payment_reference_no',
|
||||
'payment_voucher_file_ids','source_hash','source_snapshot','review_status','review_reason',
|
||||
'operation_version','updated_at')) <> 16 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Finance费用来源迁移失败:来源快照字段不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
|
||||
@@ -18,6 +18,11 @@ CREATE TABLE IF NOT EXISTS fin_fee_source (
|
||||
fee_item_name VARCHAR(200) NOT NULL COMMENT '来源费用项目名称快照',
|
||||
amount DECIMAL(20,6) NOT NULL COMMENT '来源费用金额,精度6位小数',
|
||||
currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY',
|
||||
settlement_mode VARCHAR(16) NULL COMMENT '仅PAYABLE:ON_ACCOUNT账期应付、DIRECT_PAID业务已付;RECEIVABLE为空',
|
||||
paid_at DATETIME(3) NULL COMMENT 'DIRECT_PAID业务已付时间',
|
||||
payment_method VARCHAR(32) NULL COMMENT 'DIRECT_PAID付款方式:ONLINE、BANK_TRANSFER、CASH、OTHER',
|
||||
payment_reference_no VARCHAR(128) NULL COMMENT 'DIRECT_PAID付款流水号',
|
||||
payment_voucher_file_ids JSON NULL COMMENT 'DIRECT_PAID付款凭证File ID数组',
|
||||
source_hash CHAR(64) NOT NULL COMMENT 'Firstleg费用来源完整快照SHA-256哈希',
|
||||
source_snapshot JSON NOT NULL COMMENT 'Firstleg费用来源完整不可变JSON快照',
|
||||
review_status VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT 'Finance费用状态:DRAFT草稿、PENDING待审核、APPROVED已通过、REJECTED已驳回',
|
||||
@@ -30,7 +35,16 @@ CREATE TABLE IF NOT EXISTS fin_fee_source (
|
||||
UNIQUE KEY uk_fin_fee_source (source_system, source_fee_id, source_fee_version),
|
||||
KEY idx_fin_fee_source_customer (customer_id, status),
|
||||
CONSTRAINT chk_fin_fee_source_currency CHECK (currency_code = 'CNY'),
|
||||
CONSTRAINT chk_fin_fee_source_amount CHECK (amount >= 0)
|
||||
CONSTRAINT chk_fin_fee_source_amount CHECK (amount >= 0),
|
||||
CONSTRAINT chk_fin_fee_source_settlement CHECK (
|
||||
(direction='RECEIVABLE' AND settlement_mode IS NULL AND paid_at IS NULL AND payment_method IS NULL
|
||||
AND payment_reference_no IS NULL AND payment_voucher_file_ids IS NULL)
|
||||
OR (direction='PAYABLE' AND settlement_mode='ON_ACCOUNT' AND paid_at IS NULL AND payment_method IS NULL
|
||||
AND payment_reference_no IS NULL AND payment_voucher_file_ids IS NULL)
|
||||
OR (direction='PAYABLE' AND settlement_mode='DIRECT_PAID' AND paid_at IS NOT NULL
|
||||
AND payment_method IN ('ONLINE','BANK_TRANSFER','CASH','OTHER')
|
||||
AND (payment_reference_no IS NOT NULL OR JSON_LENGTH(payment_voucher_file_ids)>0))
|
||||
)
|
||||
) ENGINE=InnoDB COMMENT='Firstleg费用来源快照池;账单人工选择并固化明细';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fin_inbox_event (
|
||||
|
||||
@@ -36,7 +36,7 @@ class FinanceFeePoolDirectionBehaviorTest {
|
||||
|
||||
assertThat(result).isNotEmpty().allMatch(item -> "PAYABLE".equals(item.direction()));
|
||||
assertThat(jdbc.sourceDirections).containsExactly("PAYABLE");
|
||||
assertThat(jdbc.costQueryCount).isEqualTo(1);
|
||||
assertThat(jdbc.costQueryCount).isZero();
|
||||
}
|
||||
|
||||
private static final class DirectionJdbc extends JdbcTemplate {
|
||||
@@ -60,7 +60,8 @@ class FinanceFeePoolDirectionBehaviorTest {
|
||||
|
||||
private static FeePoolItem item(String direction, String sourceType, long sourceId) {
|
||||
return new FeePoolItem(direction + ":" + sourceId, 7L, direction, sourceType, sourceId, 1,
|
||||
11L, 21L, null, "清关费", new BigDecimal("100.00"), "CNY", "AVAILABLE",
|
||||
11L, 21L, null, "清关费", new BigDecimal("100.00"), "CNY",
|
||||
"PAYABLE".equals(direction) ? "ON_ACCOUNT" : null, null, null, null, List.of(), "AVAILABLE",
|
||||
LocalDateTime.of(2026, 8, 11, 1, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,8 @@ class FinanceFeeSourceEventServiceTest {
|
||||
givenNewEvent();
|
||||
var snapshot = new FirstlegFeeSourceClient.Snapshot(7, "CONTAINER_FEE", 31, 1,
|
||||
null, null, null, 41L, 61L, "C-41", 7, "客户", 71L, "现有供应商",
|
||||
"TRUCKING", 61L, "TRUCKING", "拖车费", "PAYABLE", LocalDate.of(2026, 8, 11),
|
||||
"TRUCKING", "ON_ACCOUNT", null, null, null, List.of(),
|
||||
61L, "TRUCKING", "拖车费", "PAYABLE", LocalDate.of(2026, 8, 11),
|
||||
"CNY", new BigDecimal("300.00"), "APPROVED", "PUBLISHED",
|
||||
LocalDateTime.of(2026, 8, 11, 1, 0), null, null, null, null,
|
||||
"source-hash-payable", 1);
|
||||
@@ -81,7 +82,7 @@ class FinanceFeeSourceEventServiceTest {
|
||||
verify(supplierValidator).requireActive(71L, "TRUCKING");
|
||||
ArgumentCaptor<Object[]> values = ArgumentCaptor.forClass(Object[].class);
|
||||
verify(jdbc).update(startsWith("INSERT INTO fin_fee_source"), values.capture());
|
||||
assertThat(values.getValue()[14].toString())
|
||||
assertThat(values.getValue()[19].toString())
|
||||
.contains("\"supplierId\":71", "\"supplierName\":\"现有供应商\"", "\"supplierCategory\":\"TRUCKING\"");
|
||||
}
|
||||
|
||||
@@ -91,7 +92,8 @@ class FinanceFeeSourceEventServiceTest {
|
||||
givenNewEvent();
|
||||
var snapshot = new FirstlegFeeSourceClient.Snapshot(7, "CONTAINER_FEE", 31, 1,
|
||||
null, null, null, 41L, 61L, "C-41", 7, "客户", 71L, "现有供应商",
|
||||
null, 61L, "STORAGE", "仓储费", "PAYABLE", LocalDate.of(2026, 8, 11),
|
||||
null, "ON_ACCOUNT", null, null, null, List.of(),
|
||||
61L, "STORAGE", "仓储费", "PAYABLE", LocalDate.of(2026, 8, 11),
|
||||
"CNY", new BigDecimal("300.00"), "APPROVED", "PUBLISHED",
|
||||
LocalDateTime.of(2026, 8, 11, 1, 0), null, null, null, null,
|
||||
"source-hash-unrestricted", 1);
|
||||
@@ -101,6 +103,28 @@ class FinanceFeeSourceEventServiceTest {
|
||||
verify(supplierValidator).requireActive(71L, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void importsDirectPaidSnapshotWithoutTreatingItAsOnAccount() {
|
||||
FinanceFeeSourceEvent event = containerEvent("container-direct-paid-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, "现有供应商",
|
||||
"TRUCKING", "DIRECT_PAID", LocalDateTime.of(2026, 8, 11, 10, 0),
|
||||
"BANK_TRANSFER", "BANK-001", List.of(901L), 61L, "TRUCKING", "拖车费",
|
||||
"PAYABLE", LocalDate.of(2026, 8, 11), "CNY", new BigDecimal("300.00"),
|
||||
"APPROVED", "PUBLISHED", LocalDateTime.of(2026, 8, 11, 11, 0),
|
||||
null, null, null, null, "source-hash-direct", 1);
|
||||
when(firstleg.container("test-token", 41, 31, 1)).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()[13]).isEqualTo("DIRECT_PAID");
|
||||
assertThat(values.getValue()[15]).isEqualTo("BANK_TRANSFER");
|
||||
assertThat(values.getValue()[17]).isEqualTo("[901]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void adjustmentCreatesNewImmutableVersionAndSupersedesAvailableVersion() {
|
||||
FinanceFeeSourceEvent event = inboundEvent("adjusted-2", "FIRSTLEG_FEE_ADJUSTED", 2, 7);
|
||||
@@ -216,7 +240,8 @@ class FinanceFeeSourceEventServiceTest {
|
||||
String publicationStatus, BigDecimal amount) {
|
||||
return new FirstlegFeeSourceClient.Snapshot(customerId, sourceType, 31, version, inboundId,
|
||||
inboundId == null ? null : "IB-21", 51L, containerId, null,
|
||||
containerId == null ? null : "C-41", customerId, "客户", null, null, null, 61L, "CUSTOMS",
|
||||
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,
|
||||
"APPROVED", publicationStatus, LocalDateTime.of(2026, 8, 11, 1, 0),
|
||||
BigDecimal.ONE, "SHIPMENT", BigDecimal.TEN, BigDecimal.ONE, "source-hash", version);
|
||||
|
||||
@@ -14,6 +14,8 @@ class FinanceFeeSourceMigrationContractTest {
|
||||
String sql = Files.readString(Path.of("src/main/resources/db/fee-source-event-migration.sql"), StandardCharsets.UTF_8);
|
||||
assertThat(sql).contains("information_schema.columns", "source_type", "direction", "container_id",
|
||||
"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("AVAILABLE可结算", "SUPERSEDED已被新版本替代", "VOIDED已作废");
|
||||
assertThat(sql).contains("finance_fee_source_assert_schema");
|
||||
}
|
||||
|
||||
@@ -68,7 +68,8 @@ class FinancePlatformListContractTest {
|
||||
void allCustomerFeePoolResponseCarriesCustomerIdentity() {
|
||||
assertThat(Arrays.stream(com.baiyue.finance.dto.FinanceReadModels.FeePoolItem.class
|
||||
.getRecordComponents()).map(java.lang.reflect.RecordComponent::getName))
|
||||
.contains("customerId");
|
||||
.contains("customerId", "settlementMode", "paidAt", "paymentMethod",
|
||||
"paymentReferenceNo", "paymentVoucherFileIds");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -396,6 +396,22 @@ class FinanceSettlementBehaviorTest {
|
||||
verify(jdbc, never()).update(any(PreparedStatementCreator.class), any(KeyHolder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void directPaidSourceCannotEnterPayableSettlement() {
|
||||
JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class);
|
||||
when(jdbc.queryForList(startsWith("SELECT request_hash"), eq(10L), eq(7L),
|
||||
eq("SETTLEMENT_AP_CREATE"), eq("ap-direct"))).thenReturn(List.of());
|
||||
Map<String, Object> source = payableSource(10L, 20L);
|
||||
source.put("settlement_mode", "DIRECT_PAID");
|
||||
when(jdbc.queryForList(startsWith("SELECT id,customer_id"), eq(31L))).thenReturn(List.of(source));
|
||||
FinanceSettlementService service = service(jdbc, org.mockito.Mockito.mock(FinanceFileClient.class));
|
||||
|
||||
assertCode(() -> service.createPayableSettlement(7L, "ap-direct",
|
||||
new CreatePayableSettlementRequest(10L, 20L, List.of(31L),
|
||||
LocalDate.of(2026, 9, 30), "CNY")), "FIN_409");
|
||||
verify(jdbc, never()).update(startsWith("INSERT INTO fin_settlement"), any(Object[].class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void payableCreationRejectsMixedSupplierIneligibleAndOccupiedSources() {
|
||||
for (String condition : List.of("supplier", "ineligible", "occupied")) {
|
||||
@@ -649,6 +665,7 @@ class FinanceSettlementBehaviorTest {
|
||||
Map.entry("id", 31L), Map.entry("customer_id", customerId), Map.entry("direction", "PAYABLE"),
|
||||
Map.entry("currency_code", "CNY"), Map.entry("status", "AVAILABLE"),
|
||||
Map.entry("amount", new BigDecimal("100.00")), Map.entry("review_status", "APPROVED"),
|
||||
Map.entry("settlement_mode", "ON_ACCOUNT"),
|
||||
Map.entry("supplier_id", supplierId),
|
||||
Map.entry("fee_item_code", "FBA_HEAD_FREIGHT"), Map.entry("fee_item_name", "头程运费"),
|
||||
Map.entry("supplier_name", "供应商20"), Map.entry("source_snapshot",
|
||||
|
||||
@@ -44,7 +44,7 @@ class FinanceSqlCommentContractTest {
|
||||
}
|
||||
}
|
||||
assertThat(tableCount).isEqualTo(17);
|
||||
assertThat(columnCount).isEqualTo(243);
|
||||
assertThat(columnCount).isEqualTo(248);
|
||||
assertThat(missing).as("缺少中文业务注释的Finance字段").isEmpty();
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ class FinanceSqlCommentContractTest {
|
||||
assertThat(sql).contains("AVAILABLE可结算、SUPERSEDED已被新版本替代、VOIDED已作废");
|
||||
assertThat(sql).contains("RECEIVED已接收、PROCESSED已完成、FAILED处理失败");
|
||||
assertThat(sql).contains("Firstleg费用来源完整不可变JSON快照");
|
||||
assertThat(sql).contains("ON_ACCOUNT账期应付、DIRECT_PAID业务已付");
|
||||
assertThat(sql).contains("规范化完整请求的SHA-256十六进制哈希");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user