完善财务费用来源与柜级分摊
This commit is contained in:
@@ -94,7 +94,7 @@ public class BusinessFeeSourceEventService {
|
||||
number(source.containerBookingId()), trim(source.containerNo()), source.feeItemCode().trim(),
|
||||
source.feeItemName().trim(), source.totalAmount(), source.currency(), trim(source.settlementMode()),
|
||||
source.paidAt(), trim(source.paymentMethod()), trim(source.paymentReferenceNo()),
|
||||
json(source.paymentVoucherFileIds()), source.sourceHash(), json(source));
|
||||
voucherJson(source.paymentVoucherFileIds()), source.sourceHash(), json(source));
|
||||
long id = jdbc.queryForObject("SELECT id FROM fin_fee_source WHERE source_system=? AND source_type=? AND source_fee_id=? AND source_fee_version=?",
|
||||
Long.class, system, source.sourceType(), feeId, source.sourceVersion());
|
||||
if ("PAYABLE".equals(source.direction()) && "DIRECT_PAID".equals(source.settlementMode()))
|
||||
@@ -131,14 +131,14 @@ public class BusinessFeeSourceEventService {
|
||||
|| (value.paymentVoucherFileIds() != null && !value.paymentVoucherFileIds().isEmpty())) {
|
||||
throw error("FIN-EVT-012", "应收来源不得携带供应商或付款事实");
|
||||
}
|
||||
BusinessFeeSourceChangedV1 unsigned = new BusinessFeeSourceChangedV1(value.action(), value.sourceSystem(),
|
||||
BusinessFeeSourceChangedV1 unsigned = new BusinessFeeSourceChangedV1(null, value.sourceSystem(),
|
||||
value.sourceType(), value.sourceFeeId(), value.sourceVersion(), value.customerId(), value.customerName(),
|
||||
value.sourceNo(), value.businessType(), value.businessId(), value.businessNo(), value.orderId(),
|
||||
value.inboundId(), value.inboundCode(), value.containerId(), value.containerBookingId(), value.containerNo(),
|
||||
value.warehouseId(), value.feeItemCode(), value.feeItemName(), value.direction(), value.supplierId(),
|
||||
value.supplierName(), value.supplierCategory(), value.settlementMode(), value.paidAt(), value.paymentMethod(),
|
||||
value.paymentReferenceNo(), value.paymentVoucherFileIds(), value.businessDate(), value.currency(),
|
||||
value.totalAmount(), value.reviewStatus(), value.publicationStatus(), value.sourceStatus(), null);
|
||||
value.totalAmount(), value.reviewStatus(), "PUBLISHED", "ACTIVE", null);
|
||||
String expected = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(objectMapper.valueToTree(unsigned)));
|
||||
if (!expected.equals(value.sourceHash())) throw error("FIN-EVT-004", "费用来源快照哈希不一致");
|
||||
}
|
||||
@@ -166,10 +166,10 @@ public class BusinessFeeSourceEventService {
|
||||
}
|
||||
|
||||
private void materializeDirectPaidCost(String system, long feeId, BusinessFeeSourceChangedV1 value) {
|
||||
jdbc.update("INSERT INTO fin_cost(tenant_id,cost_no,cost_type,cost_origin,source_system,source_type,source_fee_id,source_fee_version,source_hash,order_id,inbound_id,container_id,supplier_id,supplier_name_snapshot,fee_item_code,fee_item_name,currency_code,cost_amount,voucher_file_ids,paid_at,payment_method,payment_reference_no,status,confirmed_at,created_by,created_by_name,updated_by) VALUES(?,?,'SOURCE_DIRECT_PAID','BUSINESS_DIRECT_PAID',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED',CURRENT_TIMESTAMP(3),0,'系统',0)",
|
||||
jdbc.update("INSERT INTO fin_cost(tenant_id,cost_no,cost_type,cost_origin,source_system,source_type,source_fee_id,source_fee_version,source_hash,order_id,inbound_id,container_id,warehouse_id,supplier_id,supplier_name_snapshot,fee_item_code,fee_item_name,currency_code,cost_amount,voucher_file_ids,paid_at,payment_method,payment_reference_no,status,confirmed_at,created_by,created_by_name,updated_by) VALUES(?,?,'SOURCE_DIRECT_PAID','BUSINESS_DIRECT_PAID',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED',CURRENT_TIMESTAMP(3),0,'系统',0)",
|
||||
positive(value.customerId(), "customerId"), "DIRECT-" + system + '-' + value.sourceType() + '-' + feeId + "-V" + value.sourceVersion(),
|
||||
system, value.sourceType(), feeId, value.sourceVersion(), value.sourceHash(), number(value.orderId()),
|
||||
number(value.inboundId()), number(value.containerId()), positive(value.supplierId(), "supplierId"),
|
||||
number(value.inboundId()), number(value.containerId()), number(value.warehouseId()), positive(value.supplierId(), "supplierId"),
|
||||
value.supplierName(), value.feeItemCode(), value.feeItemName(), value.currency(), value.totalAmount(),
|
||||
json(value.paymentVoucherFileIds()), value.paidAt(), value.paymentMethod(), value.paymentReferenceNo());
|
||||
}
|
||||
@@ -187,6 +187,9 @@ public class BusinessFeeSourceEventService {
|
||||
try { return objectMapper.writeValueAsString(value == null ? List.of() : value); }
|
||||
catch (JsonProcessingException exception) { throw error("FIN-EVT-001", "费用事件序列化失败"); }
|
||||
}
|
||||
private String voucherJson(List<String> values) {
|
||||
return values == null || values.isEmpty() ? null : json(values);
|
||||
}
|
||||
private static Long number(String value) { return blank(value) ? null : positive(value, "business identifier"); }
|
||||
private static long positive(String value, String name) {
|
||||
try { long parsed = Long.parseLong(value); if (parsed > 0) return parsed; }
|
||||
|
||||
@@ -68,9 +68,9 @@ 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 customer_id,SUM(amount) amount FROM (SELECT customer_id,amount FROM fin_fee_source WHERE customer_id IN (" + placeholders + ") AND direction='PAYABLE' AND settlement_mode='ON_ACCOUNT' AND review_status='APPROVED' AND currency_code='CNY' AND amount>0 AND status NOT IN ('VOIDED','SUPERSEDED') UNION ALL SELECT tenant_id customer_id,cost_amount amount FROM fin_cost WHERE tenant_id IN (" + placeholders + ") AND cost_origin='FIRSTLEG_DIRECT_PAID' AND status='CONFIRMED') costs GROUP BY customer_id", concat(customerIds, 2))
|
||||
jdbc.queryForList("SELECT customer_id,SUM(amount) amount FROM (SELECT customer_id,amount FROM fin_fee_source WHERE customer_id IN (" + placeholders + ") AND direction='PAYABLE' AND settlement_mode='ON_ACCOUNT' AND review_status='APPROVED' AND currency_code='CNY' AND amount>0 AND status NOT IN ('VOIDED','SUPERSEDED') UNION ALL SELECT tenant_id customer_id,cost_amount amount FROM fin_cost WHERE tenant_id IN (" + placeholders + ") AND cost_origin IN ('FIRSTLEG_DIRECT_PAID','BUSINESS_DIRECT_PAID') AND status='CONFIRMED') costs GROUP BY customer_id", concat(customerIds, 2))
|
||||
.forEach(row -> metric(result, row, "totalCost", "amount"));
|
||||
jdbc.queryForList("SELECT customer_id,SUM(amount) amount FROM (SELECT tenant_id customer_id,cost_amount amount FROM fin_cost WHERE tenant_id IN (" + placeholders + ") AND cost_origin='FIRSTLEG_DIRECT_PAID' AND status='CONFIRMED' UNION ALL SELECT tenant_id customer_id,paid_amount amount FROM fin_settlement WHERE tenant_id IN (" + placeholders + ") AND direction='PAYABLE' AND status<>'VOIDED') paid_costs GROUP BY customer_id", concat(customerIds, 2))
|
||||
jdbc.queryForList("SELECT customer_id,SUM(amount) amount FROM (SELECT tenant_id customer_id,cost_amount amount FROM fin_cost WHERE tenant_id IN (" + placeholders + ") AND cost_origin IN ('FIRSTLEG_DIRECT_PAID','BUSINESS_DIRECT_PAID') AND status='CONFIRMED' UNION ALL SELECT tenant_id customer_id,paid_amount amount FROM fin_settlement WHERE tenant_id IN (" + placeholders + ") AND direction='PAYABLE' AND status<>'VOIDED') paid_costs GROUP BY customer_id", concat(customerIds, 2))
|
||||
.forEach(row -> metric(result, row, "paidCost", "amount"));
|
||||
jdbc.queryForList("SELECT customer_id,SUM(amount) amount FROM (SELECT tenant_id customer_id,outstanding_amount amount FROM fin_settlement WHERE tenant_id IN (" + placeholders + ") AND direction='PAYABLE' AND status<>'VOIDED' UNION ALL SELECT f.customer_id,f.amount FROM fin_fee_source f WHERE f.customer_id IN (" + placeholders + ") AND f.direction='PAYABLE' AND f.settlement_mode='ON_ACCOUNT' AND f.review_status='APPROVED' AND f.currency_code='CNY' AND f.amount>0 AND f.status NOT IN ('VOIDED','SUPERSEDED') AND NOT EXISTS (SELECT 1 FROM fin_settlement_line sl JOIN fin_settlement s ON s.id=sl.settlement_id AND s.tenant_id=sl.tenant_id WHERE sl.tenant_id=f.customer_id AND sl.fee_source_id=f.id AND s.direction='PAYABLE' AND s.status<>'VOIDED')) payable_obligations GROUP BY customer_id", concat(customerIds, 2))
|
||||
.forEach(row -> metric(result, row, "outstandingPayable", "amount"));
|
||||
|
||||
@@ -34,6 +34,7 @@ import java.util.Comparator;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class FinancePhase1OperationService {
|
||||
@@ -69,8 +70,8 @@ public class FinancePhase1OperationService {
|
||||
"(SELECT COALESCE(SUM(outstanding_amount),0) FROM fin_settlement ar WHERE ar.tenant_id=c.customer_id AND ar.direction='RECEIVABLE' AND ar.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.review_status='APPROVED' AND f.currency_code='CNY' AND f.amount>0 AND f.status='AVAILABLE' AND NOT EXISTS(SELECT 1 FROM fin_bill_line bl JOIN fin_bill b ON b.id=bl.bill_id AND b.tenant_id=bl.tenant_id WHERE bl.tenant_id=f.customer_id AND bl.source_id=f.id AND bl.source_version=f.source_fee_version AND b.status<>'VOIDED')) 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(amount),0) FROM fin_fee_source f WHERE f.customer_id=c.customer_id AND f.direction='PAYABLE' AND f.settlement_mode='ON_ACCOUNT' AND f.review_status='APPROVED' AND f.currency_code='CNY' AND f.amount>0 AND f.status NOT IN ('VOIDED','SUPERSEDED'))+(SELECT COALESCE(SUM(cost_amount),0) FROM fin_cost x WHERE x.tenant_id=c.customer_id AND x.cost_origin='FIRSTLEG_DIRECT_PAID' AND x.status='CONFIRMED')) total_cost," +
|
||||
"((SELECT COALESCE(SUM(cost_amount),0) FROM fin_cost x WHERE x.tenant_id=c.customer_id AND x.cost_origin='FIRSTLEG_DIRECT_PAID' AND x.status='CONFIRMED')+(SELECT COALESCE(SUM(paid_amount),0) FROM fin_settlement x WHERE x.tenant_id=c.customer_id AND x.direction='PAYABLE' AND x.status<>'VOIDED')) paid_cost," +
|
||||
"((SELECT COALESCE(SUM(amount),0) FROM fin_fee_source f WHERE f.customer_id=c.customer_id AND f.direction='PAYABLE' AND f.settlement_mode='ON_ACCOUNT' AND f.review_status='APPROVED' AND f.currency_code='CNY' AND f.amount>0 AND f.status NOT IN ('VOIDED','SUPERSEDED'))+(SELECT COALESCE(SUM(cost_amount),0) FROM fin_cost x WHERE x.tenant_id=c.customer_id AND x.cost_origin IN ('FIRSTLEG_DIRECT_PAID','BUSINESS_DIRECT_PAID') AND x.status='CONFIRMED')) total_cost," +
|
||||
"((SELECT COALESCE(SUM(cost_amount),0) FROM fin_cost x WHERE x.tenant_id=c.customer_id AND x.cost_origin IN ('FIRSTLEG_DIRECT_PAID','BUSINESS_DIRECT_PAID') AND x.status='CONFIRMED')+(SELECT COALESCE(SUM(paid_amount),0) FROM fin_settlement x WHERE x.tenant_id=c.customer_id AND x.direction='PAYABLE' AND x.status<>'VOIDED')) paid_cost," +
|
||||
"((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')+(SELECT COALESCE(SUM(f.amount),0) FROM fin_fee_source f WHERE f.customer_id=c.customer_id AND f.direction='PAYABLE' AND f.settlement_mode='ON_ACCOUNT' AND f.review_status='APPROVED' AND f.currency_code='CNY' AND f.amount>0 AND f.status NOT IN ('VOIDED','SUPERSEDED') AND NOT EXISTS(SELECT 1 FROM fin_settlement_line sl JOIN fin_settlement s ON s.id=sl.settlement_id AND s.tenant_id=sl.tenant_id WHERE sl.tenant_id=f.customer_id AND sl.fee_source_id=f.id AND s.direction='PAYABLE' AND s.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'),COALESCE((SELECT MAX(updated_at) FROM fin_fee_source f WHERE f.customer_id=c.customer_id),'1970-01-01'),COALESCE((SELECT MAX(updated_at) FROM fin_settlement 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);
|
||||
@@ -89,7 +90,7 @@ public class FinancePhase1OperationService {
|
||||
List<RawLedger> rows = new ArrayList<>();
|
||||
rows.addAll(jdbc.query("SELECT id,bill_no,created_at,total_amount,created_by,created_by_name,remark FROM fin_bill WHERE tenant_id=? AND status<>'VOIDED'", (rs, n) -> raw(rs, "SETTLEMENT_ISSUED", "BILL", "bill_no", "total_amount", null), customerId));
|
||||
rows.addAll(jdbc.query("SELECT id,receipt_no,received_at received_time,received_amount,created_by,created_by_name,remark FROM fin_receipt WHERE tenant_id=? AND status='CONFIRMED'", (rs, n) -> raw(rs, "RECEIPT", "RECEIPT", "receipt_no", null, "received_amount"), customerId));
|
||||
rows.addAll(jdbc.query("SELECT id,cost_no,created_at,cost_amount,created_by,created_by_name,remark FROM fin_cost WHERE tenant_id=? AND cost_origin='FIRSTLEG_DIRECT_PAID' AND status='CONFIRMED'", (rs, n) -> raw(rs, "SUPPLIER_DIRECT_PAID", "DIRECT_PAID_COST", "cost_no", null, null), customerId));
|
||||
rows.addAll(jdbc.query("SELECT id,cost_no,created_at,cost_amount,created_by,created_by_name,remark FROM fin_cost WHERE tenant_id=? AND cost_origin IN ('FIRSTLEG_DIRECT_PAID','BUSINESS_DIRECT_PAID') AND status='CONFIRMED'", (rs, n) -> raw(rs, "SUPPLIER_DIRECT_PAID", "DIRECT_PAID_COST", "cost_no", null, null), customerId));
|
||||
rows.addAll(jdbc.query("SELECT p.id,CONCAT(s.settlement_no,'-',p.id) payment_no,p.created_at,p.amount cost_amount,p.created_by,p.created_by_name,p.remark FROM fin_settlement_payment p JOIN fin_settlement s ON s.id=p.settlement_id AND s.tenant_id=p.tenant_id WHERE p.tenant_id=? AND p.transaction_type='PAYMENT' AND s.direction='PAYABLE' AND s.status<>'VOIDED'", (rs, n) -> raw(rs, "SUPPLIER_ON_ACCOUNT_PAYMENT", "AP_PAYMENT", "payment_no", null, null), customerId));
|
||||
rows.sort(Comparator.comparing(RawLedger::occurredAt).thenComparing(RawLedger::id));
|
||||
BigDecimal outstanding = BigDecimal.ZERO;
|
||||
@@ -185,8 +186,8 @@ public class FinancePhase1OperationService {
|
||||
throw error("FIN-REQ-400", adjustment ? "费用调整请求无效" : "费用编辑请求无效");
|
||||
}
|
||||
FeeSourceDetail before = getFinanceFeeSource(feeSourceId);
|
||||
if ("FIRSTLEG".equals(before.sourceSystem())) {
|
||||
throw error("FIN-FEE-409", "Firstleg费用必须在入库或柜次费用明细中调整,Finance不可直接修改业务金额");
|
||||
if (Set.of("FIRSTLEG", "LASTLEG").contains(before.sourceSystem())) {
|
||||
throw error("FIN-FEE-409", "业务费用必须回到对应业务费用明细调整,Finance不可直接修改来源金额");
|
||||
}
|
||||
String operation = adjustment ? "FEE_SOURCE_ADJUST" : "FEE_SOURCE_EDIT";
|
||||
String path = "/api/platform/finance/v1/fee-sources/" + feeSourceId + (adjustment ? "/adjust" : "");
|
||||
@@ -222,6 +223,9 @@ public class FinancePhase1OperationService {
|
||||
throw error("FIN-REQ-400", "费用操作请求无效");
|
||||
}
|
||||
FeeSourceDetail before = getFinanceFeeSource(feeSourceId);
|
||||
if ("LASTLEG".equals(before.sourceSystem())) {
|
||||
throw error("FIN-FEE-409", "海外业务费用必须在海外费用来源中作废,Finance只消费作废事件");
|
||||
}
|
||||
if (!"FIRSTLEG".equals(before.sourceSystem())) {
|
||||
return transitionFeeSource(feeSourceId, operatorId, key, request, "FEE_SOURCE_VOID",
|
||||
"VOIDED", List.of("DRAFT", "PENDING", "REJECTED", "APPROVED"), true);
|
||||
|
||||
@@ -74,8 +74,8 @@ public class FinanceQueryService {
|
||||
"AND NOT EXISTS (SELECT 1 FROM fin_bill_line bl JOIN fin_bill b ON b.id=bl.bill_id AND b.tenant_id=bl.tenant_id " +
|
||||
"WHERE bl.tenant_id=f.customer_id AND bl.source_id=f.id AND b.status<>'VOIDED')) pending_billing," +
|
||||
"((SELECT COALESCE(SUM(f.amount),0) FROM fin_fee_source f WHERE f.customer_id=? AND f.direction='PAYABLE' AND f.settlement_mode='ON_ACCOUNT' AND f.review_status='APPROVED' AND f.currency_code='CNY' AND f.amount>0 AND f.status NOT IN ('VOIDED','SUPERSEDED'))+" +
|
||||
"(SELECT COALESCE(SUM(c.cost_amount),0) FROM fin_cost c WHERE c.tenant_id=? AND c.cost_origin='FIRSTLEG_DIRECT_PAID' AND c.status='CONFIRMED')) total_cost," +
|
||||
"((SELECT COALESCE(SUM(c.cost_amount),0) FROM fin_cost c WHERE c.tenant_id=? AND c.cost_origin='FIRSTLEG_DIRECT_PAID' AND c.status='CONFIRMED')+" +
|
||||
"(SELECT COALESCE(SUM(c.cost_amount),0) FROM fin_cost c WHERE c.tenant_id=? AND c.cost_origin IN ('FIRSTLEG_DIRECT_PAID','BUSINESS_DIRECT_PAID') AND c.status='CONFIRMED')) total_cost," +
|
||||
"((SELECT COALESCE(SUM(c.cost_amount),0) FROM fin_cost c WHERE c.tenant_id=? AND c.cost_origin IN ('FIRSTLEG_DIRECT_PAID','BUSINESS_DIRECT_PAID') AND c.status='CONFIRMED')+" +
|
||||
"(SELECT COALESCE(SUM(ps.paid_amount),0) FROM fin_settlement ps WHERE ps.tenant_id=? AND ps.direction='PAYABLE' AND ps.status<>'VOIDED')) paid_cost," +
|
||||
"((SELECT COALESCE(SUM(os.outstanding_amount),0) FROM fin_settlement os WHERE os.tenant_id=? AND os.direction='PAYABLE' AND os.status<>'VOIDED')+" +
|
||||
"(SELECT COALESCE(SUM(f.amount),0) FROM fin_fee_source f WHERE f.customer_id=? AND f.direction='PAYABLE' AND f.settlement_mode='ON_ACCOUNT' AND f.review_status='APPROVED' AND f.currency_code='CNY' AND f.amount>0 AND f.status NOT IN ('VOIDED','SUPERSEDED') " +
|
||||
|
||||
@@ -23,10 +23,24 @@ CALL finance_fee_source_add_column('fin_fee_source', 'source_type',
|
||||
"VARCHAR(32) NULL COMMENT '来源费用类型:INBOUND_FEE入库单费用、CONTAINER_FEE柜子费用' AFTER customer_id");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'source_no',
|
||||
"VARCHAR(64) NULL COMMENT '来源入库单号或柜号快照,可为空' AFTER source_type");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'business_type',
|
||||
"VARCHAR(32) NULL COMMENT '来源业务类型,如海外入仓、采购、卡车或柜级分摊' AFTER source_no");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'business_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '来源业务记录ID,可为空' AFTER business_type");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'business_no',
|
||||
"VARCHAR(128) NULL COMMENT '来源业务编号快照,可为空' AFTER business_id");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'inbound_code',
|
||||
"VARCHAR(64) NULL COMMENT '来源入仓码快照;并非所有费用必填' AFTER business_no");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'warehouse_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '来源海外仓或业务仓ID,可为空' AFTER inbound_code");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'direction',
|
||||
"VARCHAR(16) NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付' AFTER source_no");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'container_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '来源Firstleg装柜计划ID,可为空' AFTER inbound_id");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'container_booking_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '来源订舱柜ID,可为空' AFTER container_id");
|
||||
CALL finance_fee_source_add_column('fin_fee_source', 'container_no',
|
||||
"VARCHAR(64) NULL COMMENT '真实集装箱号快照,可为空' AFTER container_booking_id");
|
||||
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',
|
||||
@@ -77,6 +91,8 @@ CALL finance_fee_source_add_column('fin_cost', 'source_fee_version',
|
||||
"INT UNSIGNED NULL COMMENT '来源费用版本' AFTER source_fee_id");
|
||||
CALL finance_fee_source_add_column('fin_cost', 'source_hash',
|
||||
"CHAR(64) NULL COMMENT '来源不可变快照SHA-256' AFTER source_fee_version");
|
||||
CALL finance_fee_source_add_column('fin_cost', 'warehouse_id',
|
||||
"BIGINT UNSIGNED NULL COMMENT '业务直接已付成本关联仓库ID,可为空' AFTER container_id");
|
||||
CALL finance_fee_source_add_column('fin_cost', 'paid_at',
|
||||
"DATETIME(3) NULL COMMENT '直接已付时间' AFTER voucher_file_ids");
|
||||
CALL finance_fee_source_add_column('fin_cost', 'payment_method',
|
||||
@@ -86,6 +102,22 @@ CALL finance_fee_source_add_column('fin_cost', 'payment_reference_no',
|
||||
|
||||
DROP PROCEDURE IF EXISTS finance_fee_source_add_column;
|
||||
|
||||
SET @finance_fee_source_index_ddl = (
|
||||
SELECT IF(
|
||||
(SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema=DATABASE() AND table_name='fin_fee_source'
|
||||
AND index_name='uk_fin_fee_source')=4,
|
||||
'SELECT 1',
|
||||
IF(EXISTS(SELECT 1 FROM information_schema.statistics
|
||||
WHERE table_schema=DATABASE() AND table_name='fin_fee_source' AND index_name='uk_fin_fee_source'),
|
||||
'ALTER TABLE fin_fee_source DROP INDEX uk_fin_fee_source, ADD UNIQUE KEY uk_fin_fee_source(source_system,source_type,source_fee_id,source_fee_version)',
|
||||
'ALTER TABLE fin_fee_source ADD UNIQUE KEY uk_fin_fee_source(source_system,source_type,source_fee_id,source_fee_version)')
|
||||
)
|
||||
);
|
||||
PREPARE finance_fee_source_index_stmt FROM @finance_fee_source_index_ddl;
|
||||
EXECUTE finance_fee_source_index_stmt;
|
||||
DEALLOCATE PREPARE finance_fee_source_index_stmt;
|
||||
|
||||
DROP PROCEDURE IF EXISTS finance_fee_source_add_index;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE finance_fee_source_add_index()
|
||||
@@ -104,7 +136,9 @@ CALL finance_fee_source_add_index();
|
||||
DROP PROCEDURE IF EXISTS finance_fee_source_add_index;
|
||||
|
||||
ALTER TABLE fin_cost
|
||||
MODIFY COLUMN cost_type VARCHAR(24) NOT NULL COMMENT '成本归属类型:SINGLE_ORDER单票、CONTAINER_SHARED整柜公共、SOURCE_DIRECT_PAID来源直接已付';
|
||||
MODIFY COLUMN cost_type VARCHAR(24) NOT NULL COMMENT '成本归属类型:SINGLE_ORDER单票、CONTAINER_SHARED整柜公共、SOURCE_DIRECT_PAID来源直接已付',
|
||||
MODIFY COLUMN cost_origin VARCHAR(24) NOT NULL DEFAULT 'MANUAL' COMMENT '成本来源:MANUAL人工补充、FIRSTLEG_DIRECT_PAID头程直接已付、BUSINESS_DIRECT_PAID其他业务直接已付',
|
||||
MODIFY COLUMN source_hash VARCHAR(71) NULL COMMENT '来源不可变快照SHA-256,兼容sha256前缀格式';
|
||||
DROP PROCEDURE IF EXISTS finance_fee_source_replace_cost_checks;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE finance_fee_source_replace_cost_checks()
|
||||
@@ -121,7 +155,7 @@ BEGIN
|
||||
ADD CONSTRAINT chk_fin_cost_target CHECK (
|
||||
(cost_type='SINGLE_ORDER' AND order_id IS NOT NULL AND container_id IS NULL)
|
||||
OR (cost_type='CONTAINER_SHARED' AND container_id IS NOT NULL)
|
||||
OR (cost_type='SOURCE_DIRECT_PAID' AND (order_id IS NOT NULL OR inbound_id IS NOT NULL OR container_id IS NOT NULL))
|
||||
OR (cost_type='SOURCE_DIRECT_PAID' AND (order_id IS NOT NULL OR inbound_id IS NOT NULL OR container_id IS NOT NULL OR warehouse_id IS NOT NULL))
|
||||
),
|
||||
ADD CONSTRAINT chk_fin_cost_origin CHECK (
|
||||
(cost_origin='MANUAL' AND source_system IS NULL AND source_type IS NULL AND source_fee_id IS NULL AND source_fee_version IS NULL AND source_hash IS NULL)
|
||||
@@ -129,6 +163,10 @@ BEGIN
|
||||
AND source_fee_id IS NOT NULL AND source_fee_version IS NOT NULL AND source_hash IS NOT NULL
|
||||
AND status IN ('CONFIRMED','VOIDED') AND paid_at IS NOT NULL
|
||||
AND payment_method IN ('ONLINE','BANK_TRANSFER','CASH','OTHER'))
|
||||
OR (cost_origin='BUSINESS_DIRECT_PAID' AND source_system NOT IN ('', 'FIRSTLEG') AND source_type IS NOT NULL
|
||||
AND source_fee_id IS NOT NULL AND source_fee_version IS NOT NULL AND source_hash IS NOT NULL
|
||||
AND status IN ('CONFIRMED','VOIDED') AND paid_at IS NOT NULL
|
||||
AND payment_method IN ('ONLINE','BANK_TRANSFER','CASH','OTHER'))
|
||||
);
|
||||
END$$
|
||||
DELIMITER ;
|
||||
@@ -152,12 +190,12 @@ ALTER TABLE fin_fee_source
|
||||
MODIFY COLUMN source_type VARCHAR(32) NOT NULL COMMENT '来源费用类型:INBOUND_FEE入库单费用、CONTAINER_FEE柜子费用',
|
||||
MODIFY COLUMN direction VARCHAR(16) NOT NULL COMMENT '费用方向:RECEIVABLE应收、PAYABLE应付',
|
||||
MODIFY COLUMN fee_item_code VARCHAR(64) NOT NULL COMMENT '来源费用项目编码快照',
|
||||
MODIFY COLUMN source_hash CHAR(64) NOT NULL COMMENT 'Firstleg费用来源完整快照SHA-256哈希',
|
||||
MODIFY COLUMN source_snapshot JSON NOT NULL COMMENT 'Firstleg费用来源完整不可变JSON快照',
|
||||
MODIFY COLUMN source_hash VARCHAR(71) NOT NULL COMMENT '业务费用来源完整快照SHA-256哈希,兼容sha256前缀格式',
|
||||
MODIFY COLUMN source_snapshot JSON NOT NULL COMMENT '业务费用来源完整不可变JSON快照',
|
||||
MODIFY COLUMN status VARCHAR(16) NOT NULL DEFAULT 'AVAILABLE' COMMENT '费用来源状态:AVAILABLE可结算、SUPERSEDED已被新版本替代、VOIDED已作废',
|
||||
MODIFY COLUMN updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '来源状态最后更新时间,数据修改时自动刷新';
|
||||
ALTER TABLE fin_inbox_event
|
||||
MODIFY COLUMN payload_hash CHAR(64) NOT NULL COMMENT '规范化事件内容SHA-256哈希,用于同事件ID冲突检测',
|
||||
MODIFY COLUMN payload_hash VARCHAR(71) NOT NULL COMMENT '规范化事件内容SHA-256哈希,兼容sha256前缀格式',
|
||||
MODIFY COLUMN status VARCHAR(16) NOT NULL DEFAULT 'RECEIVED' COMMENT '事件处理状态:RECEIVED已接收、PROCESSED已完成、FAILED处理失败';
|
||||
|
||||
DROP PROCEDURE IF EXISTS finance_fee_source_assert_schema;
|
||||
@@ -187,7 +225,7 @@ BEGIN
|
||||
WHERE table_schema = DATABASE() AND table_name = 'fin_cost'
|
||||
AND column_name IN ('cost_origin','source_system','source_type','source_fee_id',
|
||||
'source_fee_version','source_hash','paid_at','payment_method',
|
||||
'payment_reference_no')) <> 9 THEN
|
||||
'payment_reference_no','warehouse_id')) <> 10 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Finance直接已付成本字段不完整';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.statistics
|
||||
|
||||
@@ -30,7 +30,7 @@ CREATE TABLE IF NOT EXISTS fin_fee_source (
|
||||
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_hash VARCHAR(71) NOT NULL COMMENT '业务费用来源完整快照SHA-256哈希,兼容sha256前缀格式',
|
||||
source_snapshot JSON NOT NULL COMMENT 'Firstleg费用来源完整不可变JSON快照',
|
||||
review_status VARCHAR(16) NOT NULL DEFAULT 'DRAFT' COMMENT 'Finance费用状态:DRAFT草稿、PENDING待审核、APPROVED已通过、REJECTED已驳回',
|
||||
review_reason VARCHAR(500) NULL COMMENT 'Finance费用审核说明或驳回原因',
|
||||
@@ -61,7 +61,7 @@ CREATE TABLE IF NOT EXISTS fin_inbox_event (
|
||||
source_system VARCHAR(32) NOT NULL COMMENT '事件来源系统标识',
|
||||
event_type VARCHAR(64) NOT NULL COMMENT '来源事件类型',
|
||||
payload JSON NOT NULL COMMENT '来源事件原始JSON载荷快照',
|
||||
payload_hash CHAR(64) NOT NULL COMMENT '规范化事件内容SHA-256哈希,用于同事件ID冲突检测',
|
||||
payload_hash VARCHAR(71) NOT NULL COMMENT '规范化事件内容SHA-256哈希,兼容sha256前缀格式',
|
||||
result_resource_id BIGINT UNSIGNED NULL COMMENT '成功处理生成或定位的费用来源快照ID',
|
||||
error_code VARCHAR(64) NULL COMMENT '处理失败时的稳定FIN错误码',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'RECEIVED' COMMENT '事件处理状态:RECEIVED已接收、PROCESSED已完成、FAILED处理失败',
|
||||
@@ -221,21 +221,22 @@ CREATE TABLE IF NOT EXISTS fin_cost (
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
cost_no VARCHAR(64) NOT NULL COMMENT '客户范围内唯一成本编号',
|
||||
cost_type VARCHAR(24) NOT NULL COMMENT '成本归属类型:SINGLE_ORDER单票、CONTAINER_SHARED整柜公共、SOURCE_DIRECT_PAID来源直接已付',
|
||||
cost_origin VARCHAR(24) NOT NULL DEFAULT 'MANUAL' COMMENT '成本来源:MANUAL人工补充、FIRSTLEG_DIRECT_PAID头程直接已付',
|
||||
source_system VARCHAR(32) NULL COMMENT '来源系统,头程直接已付固定FIRSTLEG',
|
||||
cost_origin VARCHAR(24) NOT NULL DEFAULT 'MANUAL' COMMENT '成本来源:MANUAL人工补充、FIRSTLEG_DIRECT_PAID头程直接已付、BUSINESS_DIRECT_PAID其他业务直接已付',
|
||||
source_system VARCHAR(32) NULL COMMENT '直接已付来源系统,如FIRSTLEG或LASTLEG',
|
||||
source_type VARCHAR(32) NULL COMMENT '来源费用类型快照',
|
||||
source_fee_id BIGINT UNSIGNED NULL COMMENT '来源费用ID',
|
||||
source_fee_version INT UNSIGNED NULL COMMENT '来源费用版本',
|
||||
source_hash CHAR(64) NULL COMMENT '来源不可变快照SHA-256',
|
||||
source_hash VARCHAR(71) NULL COMMENT '来源不可变快照SHA-256,兼容sha256前缀格式',
|
||||
order_id BIGINT UNSIGNED NULL COMMENT '成本归属的头程订单ID,单票成本必填',
|
||||
inbound_id BIGINT UNSIGNED NULL COMMENT '成本关联的入库单ID,可为空',
|
||||
container_id BIGINT UNSIGNED NULL COMMENT 'Firstleg装柜计划ID,整柜公共成本必填',
|
||||
warehouse_id BIGINT UNSIGNED NULL COMMENT '业务直接已付成本关联仓库ID,可为空',
|
||||
supplier_id BIGINT UNSIGNED NULL COMMENT 'Warehouse公共往来单位或供应商引用ID',
|
||||
supplier_name_snapshot VARCHAR(200) NULL COMMENT '登记成本时供应商或成本对象名称快照',
|
||||
fee_item_code VARCHAR(64) NOT NULL COMMENT '成本费用项目编码快照',
|
||||
fee_item_name VARCHAR(200) NOT NULL COMMENT '成本费用项目名称快照',
|
||||
currency_code CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT 'ISO 4217三位币种代码,首期仅CNY',
|
||||
cost_amount DECIMAL(20,6) NOT NULL COMMENT '成本总金额;仅FIRSTLEG_DIRECT_PAID且CONFIRMED计入已线下支付供应商款',
|
||||
cost_amount DECIMAL(20,6) NOT NULL COMMENT '成本总金额;来源直接已付且CONFIRMED计入已线下支付供应商款',
|
||||
voucher_file_ids JSON NOT NULL COMMENT 'File服务财务凭证文件ID字符串数组JSON',
|
||||
paid_at DATETIME(3) NULL COMMENT '直接已付时间;人工补充成本可为空',
|
||||
payment_method VARCHAR(32) NULL COMMENT '直接已付方式:ONLINE、BANK_TRANSFER、CASH、OTHER',
|
||||
@@ -265,7 +266,7 @@ CREATE TABLE IF NOT EXISTS fin_cost (
|
||||
CONSTRAINT chk_fin_cost_target CHECK (
|
||||
(cost_type = 'SINGLE_ORDER' AND order_id IS NOT NULL AND container_id IS NULL)
|
||||
OR (cost_type = 'CONTAINER_SHARED' AND container_id IS NOT NULL)
|
||||
OR (cost_type = 'SOURCE_DIRECT_PAID' AND (order_id IS NOT NULL OR inbound_id IS NOT NULL OR container_id IS NOT NULL))
|
||||
OR (cost_type = 'SOURCE_DIRECT_PAID' AND (order_id IS NOT NULL OR inbound_id IS NOT NULL OR container_id IS NOT NULL OR warehouse_id IS NOT NULL))
|
||||
),
|
||||
CONSTRAINT chk_fin_cost_origin CHECK (
|
||||
(cost_origin='MANUAL' AND source_system IS NULL AND source_type IS NULL AND source_fee_id IS NULL AND source_fee_version IS NULL AND source_hash IS NULL)
|
||||
@@ -273,8 +274,12 @@ CREATE TABLE IF NOT EXISTS fin_cost (
|
||||
AND source_fee_id IS NOT NULL AND source_fee_version IS NOT NULL AND source_hash IS NOT NULL
|
||||
AND status IN ('CONFIRMED','VOIDED') AND paid_at IS NOT NULL
|
||||
AND payment_method IN ('ONLINE','BANK_TRANSFER','CASH','OTHER'))
|
||||
OR (cost_origin='BUSINESS_DIRECT_PAID' AND source_system NOT IN ('', 'FIRSTLEG') AND source_type IS NOT NULL
|
||||
AND source_fee_id IS NOT NULL AND source_fee_version IS NOT NULL AND source_hash IS NOT NULL
|
||||
AND status IN ('CONFIRMED','VOIDED') AND paid_at IS NOT NULL
|
||||
AND payment_method IN ('ONLINE','BANK_TRANSFER','CASH','OTHER'))
|
||||
)
|
||||
) ENGINE=InnoDB COMMENT='成本事实;人工补充成本与头程直接已付来源成本统一留痕';
|
||||
) ENGINE=InnoDB COMMENT='成本事实;人工补充成本与业务直接已付来源成本统一留痕';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fin_cost_allocation (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
|
||||
@@ -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