feat(fleet): read attributable trip receivables and receipts from finance

This commit is contained in:
2026-10-05 11:51:47 +08:00
parent 113d2b1fec
commit 7577650eee
11 changed files with 891 additions and 2 deletions
@@ -0,0 +1,23 @@
package com.baiyue.finance.controller;
import com.baiyue.finance.dto.TripFinanceModels;
import com.baiyue.finance.service.TripFinanceQueryService;
import com.baiyue.finance.service.FinanceEventException;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
class InternalTripFinanceControllerTest {
@Test void rejectsAbsentOrWrongInternalTokenBeforeReadingFinance() {
var service=mock(TripFinanceQueryService.class);var controller=new InternalTripFinanceController(service,"synthetic-token");
var query=new TripFinanceModels.Query(List.of(),false);
assertThatThrownBy(()->controller.query(null,query)).isInstanceOf(FinanceEventException.class);
assertThatThrownBy(()->controller.query("wrong",query)).isInstanceOf(FinanceEventException.class);
verifyNoInteractions(service);
}
@Test void validInternalTokenReturnsOnlyTheProviderResult() {
var service=mock(TripFinanceQueryService.class);var query=new TripFinanceModels.Query(List.of(),false);
var result=new TripFinanceModels.Result("PENDING_ALLOCATION",List.of(),List.of(),List.of(),"pending");
when(service.query(query)).thenReturn(result);
assertThat(new InternalTripFinanceController(service,"synthetic-token").query("synthetic-token",query).data()).isSameAs(result);
}
}
@@ -0,0 +1,68 @@
package com.baiyue.finance.service;
import com.baiyue.finance.dto.TripFinanceModels;
import org.junit.jupiter.api.*;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.util.*;
import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.*;
class TripFinanceQueryServiceTest {
JdbcTemplate jdbc;TripFinanceQueryService service;
@BeforeEach void setup() {
jdbc=new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa",""));
jdbc.execute("CREATE TABLE fin_fee_source(id BIGINT,customer_id BIGINT,business_no VARCHAR(64),business_type VARCHAR(32),source_no VARCHAR(64),fee_item_name VARCHAR(64),source_fee_id BIGINT,source_fee_version INT,source_system VARCHAR(32),source_type VARCHAR(32),direction VARCHAR(32),review_status VARCHAR(32),status VARCHAR(32),currency_code VARCHAR(3),amount DECIMAL(20,6))");
jdbc.execute("CREATE TABLE fin_bill(id BIGINT,tenant_id BIGINT,bill_no VARCHAR(64),currency_code VARCHAR(3),status VARCHAR(16))");
jdbc.execute("CREATE TABLE fin_bill_line(tenant_id BIGINT,bill_id BIGINT,source_id BIGINT,source_version INT,currency_code VARCHAR(3))");
jdbc.execute("CREATE TABLE fin_receipt(id BIGINT,tenant_id BIGINT,status VARCHAR(16),currency_code VARCHAR(3))");
jdbc.execute("CREATE TABLE fin_receipt_bill(tenant_id BIGINT,receipt_id BIGINT,bill_id BIGINT,applied_amount DECIMAL(20,6))");
service=new TripFinanceQueryService(jdbc);
}
void fee(long id,long customer,String business,int version,String currency,String amount,String status) {
jdbc.update("INSERT INTO fin_fee_source VALUES(?,?,?,'DELIVERY_ORDER',?,'Delivery',99,?,'LASTLEG','OVERSEAS_DELIVERY_FEE','RECEIVABLE','APPROVED',?,?,?)",id,customer,business,business,version,status,currency,new BigDecimal(amount));
}
TripFinanceModels.Query request(boolean receipts) {return new TripFinanceModels.Query(List.of(new TripFinanceModels.Business(1,"DL-A")),receipts);}
void bill(long source,int version) {
jdbc.update("INSERT INTO fin_bill VALUES(1,1,'BILL-A','CNY','ISSUED')");
jdbc.update("INSERT INTO fin_bill_line VALUES(1,1,?,?,'CNY')",source,version);
}
void receipt(String amount) {
jdbc.update("INSERT INTO fin_receipt VALUES(1,1,'CONFIRMED','CNY')");
jdbc.update("INSERT INTO fin_receipt_bill VALUES(1,1,1,?)",new BigDecimal(amount));
}
@Test void exactWholeBillUsesRecordedAllocationAmount() {
fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");bill(1,1);receipt("3.5");
var result=service.query(request(true));assertThat(result.totals().get(0).receivedAmount()).isEqualByComparingTo("3.5");
assertThat(result.totals().get(0).receivableAmount()).isEqualByComparingTo("20");
assertThat(result.documents().get(0).allocationStatus()).isEqualTo("ATTRIBUTED");
}
@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')");
var result=service.query(request(true));assertThat(result.totals().get(0).receivedAmount()).isNull();
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();
assertThat(service.query(request(true)).totals().get(0).receivableAmount()).isEqualByComparingTo("0");
}
@Test void latestVoidedDoesNotFallBackAndOtherCustomerExcluded() {
fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");fee(2,1,"DL-A",2,"CNY","20","VOIDED");
fee(3,2,"DL-A",1,"CNY","999","AVAILABLE");
var result=service.query(request(true));assertThat(result.sources()).isEmpty();assertThat(result.status()).isEqualTo("PENDING_ALLOCATION");
}
@Test void currenciesSeparateAndNoReceiptPermissionMasksDocuments() {
fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");
jdbc.update("UPDATE fin_fee_source SET source_fee_id=1 WHERE id=1");
fee(2,1,"DL-A",1,"USD","5","AVAILABLE");
var result=service.query(request(false));assertThat(result.totals()).extracting(TripFinanceModels.Money::currency).containsExactly("CNY","USD");
assertThat(result.totals()).allSatisfy(m->assertThat(m.receivedAmount()).isNull());assertThat(result.documents()).isEmpty();
}
@Test void wholeTripBillIsCountedOnceAcrossTwoFeeLines() {
fee(1,1,"DL-A",1,"CNY","20","AVAILABLE");jdbc.update("UPDATE fin_fee_source SET source_fee_id=1 WHERE id=1");
fee(2,1,"DL-A",1,"CNY","10","AVAILABLE");bill(1,1);jdbc.update("INSERT INTO fin_bill_line VALUES(1,1,2,1,'CNY')");receipt("7");
var result=service.query(request(true));assertThat(result.documents()).hasSize(1);assertThat(result.totals().get(0).receivedAmount()).isEqualByComparingTo("7");
}
}