fix(finance): retain trip receipts after partial and full payment

This commit is contained in:
2026-10-05 12:22:18 +08:00
parent 8ac2400043
commit f13d340620
3 changed files with 32 additions and 3 deletions
@@ -49,7 +49,7 @@ public class TripFinanceQueryService {
SELECT b.id,b.tenant_id,b.bill_no,b.currency_code,l.source_id FROM fin_bill_line l
JOIN fin_bill b ON b.id=l.bill_id AND b.tenant_id=l.tenant_id
JOIN fin_fee_source f ON f.id=l.source_id AND f.customer_id=l.tenant_id AND f.source_fee_version=l.source_version
WHERE l.source_id IN (:ids) AND b.status='ISSUED'
WHERE l.source_id IN (:ids) AND b.status IN ('ISSUED','PARTIALLY_PAID','PAID')
""",Map.of("ids",eligible.keySet()),(r,i)->new Object[]{
new Bill(r.getLong("id"),r.getLong("tenant_id"),r.getString("bill_no"),r.getString("currency_code")),r.getLong("source_id")});
var bills=new LinkedHashMap<Long,Bill>();var byFee=new HashMap<Long,Set<Long>>();
@@ -3,7 +3,7 @@
"info": {
"title": "车次Finance权威应收与实收只读来源",
"version": "1.0.0",
"description": "Lastleg以不可变DEPART验证后的客户/派送业务号读取。仅本Finance库;最新已审核有效LASTLEG OVERSEAS_DELIVERY_FEE应收,按币种。实收只取fin_receipt_bill已确认分配;整张ISSUED账单全部来源均明确归属请求车次时才计一次。混合账单、无分配、无有效来源为待分配,不按账单总额或比例估算。最多200业务、2000费用,批量查询。现行应收结算单收款沿关联账单,fin_settlement_payment仅用于应付,不重复叠加。"
"description": "Lastleg以不可变DEPART验证后的客户/派送业务号读取。仅本Finance库;最新已审核有效LASTLEG OVERSEAS_DELIVERY_FEE应收,按币种。实收只取fin_receipt_bill已确认分配;整张已出账账单(ISSUED、PARTIALLY_PAID、PAID)全部来源均明确归属请求车次时才计一次。混合账单、无分配、无有效来源为待分配,不按账单总额或比例估算。最多200业务、2000费用,批量查询。现行应收结算单收款沿关联账单,fin_settlement_payment仅用于应付,不重复叠加。"
},
"paths": {
"/internal/finance/v1/trip-receivables/query": {
@@ -35,17 +35,46 @@ class TripFinanceQueryServiceTest {
assertThat(result.totals().get(0).receivableAmount()).isEqualByComparingTo("20");
assertThat(result.documents().get(0).allocationStatus()).isEqualTo("ATTRIBUTED");
}
@Test void issuedPartialAndPaidBillsKeepTheirConfirmedAllocation() {
for(String status:List.of("ISSUED","PARTIALLY_PAID","PAID")) {
setup();fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");bill(1,1);
String amount="PAID".equals(status)?"20":"3.5";receipt(amount);
jdbc.update("UPDATE fin_bill SET status=?",status);
var result=service.query(request(true));
assertThat(result.documents()).as(status).hasSize(1);
assertThat(result.documents().get(0).allocationStatus()).isEqualTo("ATTRIBUTED");
assertThat(result.totals().get(0).receivedAmount()).as(status).isEqualByComparingTo(amount);
}
}
@Test void draftPendingApprovedAndVoidedBillsNeverContributeReceipts() {
for(String status:List.of("DRAFT","PENDING_REVIEW","APPROVED","VOIDED")) {
setup();fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");bill(1,1);receipt("3.5");
jdbc.update("UPDATE fin_bill SET status=?",status);
var result=service.query(request(true));assertThat(result.documents()).as(status).isEmpty();
assertThat(result.totals().get(0).receivedAmount()).isNull();
}
}
@Test void paidBillStillEnforcesCustomerVersionAndCurrencyAttribution() {
for(String change:List.of("source_version=2","tenant_id=2","currency_code='USD'")) {
setup();fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");bill(1,1);receipt("20");
jdbc.update("UPDATE fin_bill SET status='PAID'");jdbc.update("UPDATE fin_bill_line SET "+change);
assertThat(service.query(request(true)).totals().get(0).receivedAmount()).as(change).isNull();
}
}
@Test void mixedBillNeverProratesReceipt() {
fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");bill(1,1);receipt("30");
jdbc.update("INSERT INTO fin_bill_line VALUES(1,1,999,1,'CNY')");
jdbc.update("UPDATE fin_bill SET status='PAID'");
var result=service.query(request(true));assertThat(result.totals().get(0).receivedAmount()).isNull();
assertThat(result.documents()).hasSize(1);
assertThat(result.documents().get(0).billNo()).isEqualTo("BILL-A");
assertThat(result.documents().get(0).allocationStatus()).isEqualTo("PENDING_ALLOCATION");
}
@Test void unbilledOrNoConfirmedReceiptIsUnknownNotZero() {
fee(1,1,"DL-A",1,"CNY","0","AVAILABLE");
assertThat(service.query(request(true)).totals().get(0).receivedAmount()).isNull();
bill(1,1);assertThat(service.query(request(true)).totals().get(0).receivedAmount()).isNull();
bill(1,1);var unpaid=service.query(request(true));assertThat(unpaid.totals().get(0).receivedAmount()).isNull();
assertThat(unpaid.documents()).hasSize(1);assertThat(unpaid.documents().get(0).allocationStatus()).isEqualTo("PENDING_ALLOCATION");
assertThat(service.query(request(true)).totals().get(0).receivableAmount()).isEqualByComparingTo("0");
}
@Test void latestVoidedDoesNotFallBackAndOtherCustomerExcluded() {