fix(lastleg): expose dispatch reviews and filter available capacity offers

This commit is contained in:
2026-10-08 14:11:55 +08:00
parent 1cb1f2d0c0
commit 136a2bc3b5
14 changed files with 194 additions and 18 deletions
@@ -36,22 +36,50 @@ class FleetCapacityOfferServiceTest {
@AfterEach void clear(){SecurityContextHolder.clearContext();}
@Test void ordinaryPageOnlyCalculatesRequestedDatabasePageAndPreservesAccurateTotal(){
var rows=List.of(offer(1),offer(2));var page=new Page<FleetCapacityOfferEntity>(3,20,2000);page.setRecords(rows);
when(offers.pageOffers(any(),any(),eq(true))).thenReturn(page);
when(offers.pageOffers(any(),any(),eq(true),any())).thenReturn(page);
for(var row:rows){var trip=trip(row.getTripNo());when(trips.findByNo(row.getTripNo())).thenReturn(trip);
when(reservations.stops(trip)).thenReturn(List.of(new FleetTripModels.Stop("起点",null,null),new FleetTripModels.Stop("ONT8",null,"FBA:ONT8")));
when(execution.capacityPlan(row.getTripNo())).thenReturn(new FleetExecutionModels.Planning(row.getTripNo(),1L,List.of(),"hash",null,false,"计划待核对",List.of()));}
var query=new FleetCapacityOfferModels.Query(3,20,"FO",null,null,null,null,null,null,null);
var query=new FleetCapacityOfferModels.Query(3,20,"FO",null,null,null,null,null,null,null,null);
var result=service.page(query);
assertThat(result.total()).isEqualTo(2000);assertThat(result.page()).isEqualTo(3);assertThat(result.records()).hasSize(2);
var captor=ArgumentCaptor.forClass(Page.class);verify(offers).pageOffers(captor.capture(),same(query),eq(true));
assertThat(result.records()).allMatch(FleetCapacityOfferModels.Offer::canCreateInquiry).allMatch(value -> !value.canConfirm());
var captor=ArgumentCaptor.forClass(Page.class);verify(offers).pageOffers(captor.capture(),same(query),eq(true),eq(LocalDateTime.ofInstant(now,ZoneOffset.UTC)));
assertThat(captor.getValue().getCurrent()).isEqualTo(3);assertThat(captor.getValue().getSize()).isEqualTo(20);
verify(execution,times(2)).capacityPlan(anyString());verify(trips,times(2)).findByNo(anyString());verifyNoMoreInteractions(execution,trips);
}
@Test void broadDynamicAvailabilityFilterRejectsBeforeAnyPerTripCapacityQuery(){
when(offers.pageOffers(any(),any(),eq(true))).thenReturn(new Page<>(1,100,101));
assertThatThrownBy(()->service.page(new FleetCapacityOfferModels.Query(1,20,null,null,null,null,null,null,true,null))).hasMessageContaining("100");
when(offers.pageOffers(any(),any(),eq(true),any())).thenReturn(new Page<>(1,100,101));
assertThatThrownBy(()->service.page(new FleetCapacityOfferModels.Query(1,20,null,null,null,null,null,null,true,null,null))).hasMessageContaining("100");
verifyNoInteractions(execution,trips);
}
@Test void historyScopeRequiresManagementAndDefaultScopeIsAvailable(){
var query=new FleetCapacityOfferModels.Query(1,20,null,null,null,null,null,null,null,null,null);
assertThat(query.scope()).isEqualTo("AVAILABLE");
var history=new FleetCapacityOfferModels.Query(1,20,null,null,null,null,null,null,null,null,"ALL");
assertThatThrownBy(()->service.page(history)).isInstanceOf(org.springframework.web.server.ResponseStatusException.class);
verifyNoInteractions(offers);
var user=new AuthenticatedUser(101L,0L,0L,101L,"admin","ADMIN","WEB",Set.of(FleetCapacityOfferService.MANAGE));
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user,"",List.of(new SimpleGrantedAuthority(FleetCapacityOfferService.MANAGE))));
when(offers.pageOffers(any(),same(query),eq(true),any())).thenReturn(new Page<>(1,20,0));
service.page(query);
verify(offers).pageOffers(any(),same(query),eq(true),eq(LocalDateTime.ofInstant(now,ZoneOffset.UTC)));
when(offers.pageOffers(any(),same(history),eq(false),any())).thenReturn(new Page<>(1,20,0));
assertThat(service.page(history).records()).isEmpty();
verify(offers).pageOffers(any(),same(history),eq(false),eq(LocalDateTime.ofInstant(now,ZoneOffset.UTC)));
}
@Test void availableSqlFiltersBeforePaginationWithoutRequiringKnownCapacity() throws Exception {
var method=Arrays.stream(FleetCapacityOfferMapper.class.getMethods()).filter(m->m.getName().equals("pageOffers")).findFirst().orElseThrow();
String script=String.join("\n",method.getAnnotation(org.apache.ibatis.annotations.Select.class).value());
var sql=new org.apache.ibatis.scripting.xmltags.XMLLanguageDriver().createSqlSource(new org.apache.ibatis.session.Configuration(),script,Map.class);
var query=new FleetCapacityOfferModels.Query(1,20,null,null,null,null,null,null,null,null,null);
var parameters=new HashMap<String,Object>();parameters.put("q",query);parameters.put("availableOnly",true);parameters.put("now",LocalDateTime.ofInstant(now,ZoneOffset.UTC));
String active=sql.getBoundSql(parameters).getSql().replaceAll("\\s+"," ");
assertThat(active).contains("o.status='OPEN'", "t.status='PLANNED'", "o.cutoff_at > ?", "o.cutoff_at <= t.planned_departure_at");
assertThat(active).doesNotContain("planned_load_json", "remaining", "NOW()");
parameters.put("availableOnly",false);
assertThat(sql.getBoundSql(parameters).getSql()).doesNotContain("o.cutoff_at", "t.status='PLANNED'");
}
@Test void cutoffUsesStrictServerInstantAndRechecksChangedDeparture(){
var trip=trip("FT-A");var offer=offer(1);offer.setStatus("OPEN");
assertThat(FleetCapacityOfferService.openBlock(offer,trip,now)).isNull();
@@ -0,0 +1,33 @@
package com.baiyue.lastleg.fleet.trip;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.any;
class FleetDispatchWorkflowQueryTest {
@Test
void batchesReviewsAndKeepsCustomerDispatchDecisionsIndependent() {
var bindings = mock(FleetTripDispatchMapper.class);
var trips = mock(FleetTripMapper.class);
var first = new FleetTripDispatch(); first.setDispatchNo("PC-A"); first.setTripId(1L); first.setTripNo("TR-A");
first.setReviewJson("{\"status\":\"REJECTED\",\"reason\":\"货物需重新核对\"}");
var second = new FleetTripDispatch(); second.setDispatchNo("PC-B"); second.setTripId(1L); second.setTripNo("TR-A");
var trip = new FleetTripEntity(); trip.setId(1L);
trip.setLoadingReviewJson("{\"status\":\"APPROVED\",\"reason\":\"审核通过\"}");
when(bindings.findByDispatchNos(List.of("PC-A", "PC-B", "PC-C"))).thenReturn(List.of(first, second));
when(trips.selectBatchIds(any())).thenReturn(List.of(trip));
var query = new FleetDispatchWorkflowQuery(bindings, trips, new ObjectMapper().findAndRegisterModules());
var result = query.read(List.of("PC-A", "PC-B", "PC-C"));
assertEquals("REJECTED", result.get("PC-A").dispatchReview().status());
assertEquals("货物需重新核对", result.get("PC-A").dispatchReview().reason());
assertEquals("PENDING", result.get("PC-B").dispatchReview().status());
assertEquals("APPROVED", result.get("PC-B").loadingReview().status());
assertFalse(result.containsKey("PC-C"));
verify(trips).selectBatchIds(List.of(1L));
verifyNoMoreInteractions(trips);
assertTrue(query.read(List.of()).isEmpty());
}
}