feat(fleet): read attributable trip receivables and receipts from finance
This commit is contained in:
+101
@@ -0,0 +1,101 @@
|
||||
package com.baiyue.lastleg.fleet.reporting;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import java.util.*;
|
||||
import java.math.BigDecimal;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
class TripOperationsFinanceServiceTest {
|
||||
static class Reads extends JdbcTemplate {
|
||||
final Queue<List<?>> results=new ArrayDeque<>();final List<String> sql=new ArrayList<>();
|
||||
@Override @SuppressWarnings("unchecked") public <T> List<T> query(String statement,RowMapper<T> mapper,Object...args){sql.add(statement);return (List<T>)results.remove();}
|
||||
}
|
||||
static TripOperationsFinanceService.Ownership owner(String customer,String dispatch,String trip) {return new TripOperationsFinanceService.Ownership(new TripOperationsFinanceService.Binding(customer,dispatch),1,trip);}
|
||||
static TripOperationsFinanceService.Delivery delivery(String customer,String dispatch,String no) {return new TripOperationsFinanceService.Delivery(new TripOperationsFinanceService.Binding(customer,dispatch),no);}
|
||||
TripFinanceClient.Result value() {return new TripFinanceClient.Result("AVAILABLE",List.of(new TripFinanceClient.Money("CNY",new BigDecimal("20"),null)),List.of(new TripFinanceClient.Source("1","DL-A","FEE-A","Delivery",1,"CNY",new BigDecimal("20"),"AVAILABLE")),List.of(),"actual source");}
|
||||
@Test void readsFrozenCustomerDispatchThenFinanceWithoutCrossDatabase() {
|
||||
var jdbc=new Reads();jdbc.results.add(List.of(TripOperationsServiceTest.DEPARTURE));
|
||||
jdbc.results.add(List.of(owner("1","D-A","TR-A"),owner("2","D-B","TR-A")));
|
||||
jdbc.results.add(List.of(delivery("1","D-A","DL-A"),delivery("2","D-B","DL-B")));
|
||||
var client=mock(TripFinanceClient.class);when(client.query(anyString(),any())).thenReturn(ApiResponse.success(value()));
|
||||
var service=new TripOperationsFinanceService(jdbc,new ObjectMapper().registerModule(new JavaTimeModule()),client,"test-token");
|
||||
var result=service.read("TR-A",false);assertThat(result.attributionStatus()).isEqualTo("ATTRIBUTED");
|
||||
verify(client).query("test-token",new TripFinanceClient.Query(List.of(new TripFinanceClient.Business(1,"DL-A"),new TripFinanceClient.Business(2,"DL-B")),false));
|
||||
assertThat(jdbc.sql).allSatisfy(sql->assertThat(sql).doesNotContain("fin_fee_source","fin_receipt"));
|
||||
}
|
||||
@Test void dispatchUsedByAnotherDepartureIsNotAssignedAgain() {
|
||||
var jdbc=new Reads();jdbc.results.add(List.of(TripOperationsServiceTest.DEPARTURE));
|
||||
jdbc.results.add(List.of(new TripOperationsFinanceService.Ownership(new TripOperationsFinanceService.Binding("1","D-A"),2,"TR-OTHER"),new TripOperationsFinanceService.Ownership(new TripOperationsFinanceService.Binding("2","D-B"),2,"TR-OTHER")));
|
||||
jdbc.results.add(List.of(delivery("1","D-A","DL-A"),delivery("2","D-B","DL-B")));
|
||||
var client=mock(TripFinanceClient.class);when(client.query(anyString(),any())).thenReturn(ApiResponse.success(new TripFinanceClient.Result("PENDING_ALLOCATION",List.of(),List.of(),List.of(),"pending")));
|
||||
var result=new TripOperationsFinanceService(jdbc,new ObjectMapper().registerModule(new JavaTimeModule()),client,"test-token").read("TR-A",true);
|
||||
assertThat(result.attributionStatus()).isEqualTo("PENDING_ALLOCATION");verify(client).query("test-token",new TripFinanceClient.Query(List.of(),true));
|
||||
}
|
||||
@Test void databaseQueryCountIsConstantForOneTwoAndTwoHundredBindings() throws Exception {
|
||||
for(int count:List.of(1,2,200)) {
|
||||
var json=new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
var snapshot=json.readTree(TripOperationsServiceTest.DEPARTURE);var cargo=json.createArrayNode();
|
||||
var owners=new ArrayList<TripOperationsFinanceService.Ownership>();var deliveries=new ArrayList<TripOperationsFinanceService.Delivery>();
|
||||
var expected=new ArrayList<TripFinanceClient.Business>();
|
||||
for(int i=1;i<=count;i++) {
|
||||
var item=json.createObjectNode().put("customerId",Integer.toString(i)).put("dispatchNo","D-"+i);
|
||||
cargo.add(item);owners.add(owner(Integer.toString(i),"D-"+i,"TR-A"));
|
||||
deliveries.add(delivery(Integer.toString(i),"D-"+i,"DL-"+i));expected.add(new TripFinanceClient.Business(i,"DL-"+i));
|
||||
}
|
||||
((com.fasterxml.jackson.databind.node.ObjectNode)snapshot.get("confirmedLoad")).set("cargo",cargo);
|
||||
var jdbc=new Reads();jdbc.results.add(List.of(json.writeValueAsString(snapshot)));jdbc.results.add(owners);jdbc.results.add(deliveries);
|
||||
var client=mock(TripFinanceClient.class);when(client.query(anyString(),any())).thenReturn(ApiResponse.success(new TripFinanceClient.Result("AVAILABLE",List.of(),List.of(),List.of(),"authority")));
|
||||
var result=new TripOperationsFinanceService(jdbc,json,client,"test-token").read("TR-A",false);
|
||||
assertThat(result.attributionStatus()).isEqualTo("ATTRIBUTED");assertThat(jdbc.sql).hasSize(3);assertThat(jdbc.results).isEmpty();
|
||||
verify(client,times(1)).query("test-token",new TripFinanceClient.Query(expected,false));
|
||||
}
|
||||
}
|
||||
@Test void batchDeliverySqlExecutesExactPairsWithoutCartesianCrossCustomerLeak() {
|
||||
var data=new org.springframework.jdbc.datasource.DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");
|
||||
class DatabaseReads extends JdbcTemplate {
|
||||
int queries;
|
||||
DatabaseReads(){super(data);}
|
||||
@Override @SuppressWarnings("unchecked") public <T> List<T> query(String statement,RowMapper<T> mapper,Object...args) {
|
||||
queries++;
|
||||
if(statement.contains("JSON_TABLE")) {
|
||||
assertThat(args).containsExactly("1","D-A","2","D-B");
|
||||
return (List<T>)List.of(owner("1","D-A","TR-A"),owner("2","D-B","TR-A"));
|
||||
}
|
||||
return super.query(statement,mapper,args);
|
||||
}
|
||||
}
|
||||
var jdbc=new DatabaseReads();
|
||||
jdbc.execute("CREATE TABLE ll_fleet_trip(id BIGINT,trip_no VARCHAR(64),departed_at TIMESTAMP)");
|
||||
jdbc.execute("CREATE TABLE ll_fleet_trip_history(trip_id BIGINT,action VARCHAR(32),record_version BIGINT,snapshot_json VARCHAR(10000))");
|
||||
jdbc.execute("CREATE TABLE ll_delivery_order(customer_id VARCHAR(32),dispatch_no VARCHAR(64),delivery_no VARCHAR(64))");
|
||||
jdbc.update("INSERT INTO ll_fleet_trip VALUES(1,'TR-A',CURRENT_TIMESTAMP)");
|
||||
jdbc.update("INSERT INTO ll_fleet_trip_history VALUES(1,'DEPART',1,?)",TripOperationsServiceTest.DEPARTURE);
|
||||
jdbc.update("INSERT INTO ll_delivery_order VALUES('1','D-A','DL-A'),('2','D-B','DL-B'),('1','D-B','WRONG-CUSTOMER-A'),('2','D-A','WRONG-CUSTOMER-B')");
|
||||
var client=mock(TripFinanceClient.class);when(client.query(anyString(),any())).thenReturn(ApiResponse.success(value()));
|
||||
new TripOperationsFinanceService(jdbc,new ObjectMapper().registerModule(new JavaTimeModule()),client,"test-token").read("TR-A",false);
|
||||
assertThat(jdbc.queries).isEqualTo(3);
|
||||
verify(client).query("test-token",new TripFinanceClient.Query(List.of(new TripFinanceClient.Business(1,"DL-A"),new TripFinanceClient.Business(2,"DL-B")),false));
|
||||
}
|
||||
@Test void moreThanTwoHundredBindingsStopsBeforeBatchReadsOrFinance() throws Exception {
|
||||
var json=new ObjectMapper().registerModule(new JavaTimeModule());var snapshot=json.readTree(TripOperationsServiceTest.DEPARTURE);var cargo=json.createArrayNode();
|
||||
for(int i=1;i<=201;i++)cargo.add(json.createObjectNode().put("customerId",Integer.toString(i)).put("dispatchNo","D-"+i));
|
||||
((com.fasterxml.jackson.databind.node.ObjectNode)snapshot.get("confirmedLoad")).set("cargo",cargo);
|
||||
var jdbc=new Reads();jdbc.results.add(List.of(json.writeValueAsString(snapshot)));var client=mock(TripFinanceClient.class);
|
||||
assertThatThrownBy(()->new TripOperationsFinanceService(jdbc,json,client,"test-token").read("TR-A",false)).isInstanceOf(org.springframework.web.server.ResponseStatusException.class);
|
||||
assertThat(jdbc.sql).hasSize(1);verifyNoInteractions(client);
|
||||
}
|
||||
@Test void mismatchedCustomerAndUnauthorizedReceiptAreRejected() {
|
||||
assertThatThrownBy(()->TripOperationsFinanceService.validate(value(),List.of(new TripFinanceClient.Business(2,"DL-A")),false)).isInstanceOf(IllegalStateException.class);
|
||||
var withReceipt=new TripFinanceClient.Result("AVAILABLE",List.of(new TripFinanceClient.Money("CNY",BigDecimal.ONE,BigDecimal.ONE)),List.of(),List.of(),"actual");
|
||||
assertThatThrownBy(()->TripOperationsFinanceService.validate(withReceipt,List.of(),false)).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
@Test void financeEndpointRequiresOperationsAndExistingReceivablePermission() throws Exception {
|
||||
var method=Arrays.stream(PlatformTripOperationsController.class.getMethods()).filter(m->m.getName().equals("finance")).findFirst().orElseThrow();
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value()).contains("platform:fleet:operations:list","platform:finance:customer-arap:list");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user