fix(lastleg): expose dispatch reviews and filter available capacity offers
This commit is contained in:
+1
-1
@@ -38,7 +38,7 @@ class DispatchQueryControllerTest {
|
||||
|
||||
@Test
|
||||
void platformPageQueriesAcrossCustomersWithoutCustomerId() {
|
||||
DispatchRequests.Query query = new DispatchRequests.Query(1, 20, null, null, null, null);
|
||||
DispatchRequests.Query query = new DispatchRequests.Query(1, 20, null, null, null, null, null);
|
||||
when(service.platformPage(query)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||
|
||||
var response = controller.page(platform(), query);
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ class DispatchBolPrintServiceTest {
|
||||
null, null, null, LocalDateTime.of(2026, 9, 12, 2, 40, 20), "PENDING_DEPARTURE",
|
||||
1, 1, 1, 0, 0, 1, null, LocalDateTime.of(2026, 9, 11, 10, 0), List.of(),
|
||||
List.of(new DispatchResponse.BolLine("TSTU3539519", "DP-260911-0027", "ABE2", "ABE2",
|
||||
"NYEXX2609110214", null, null, 1)));
|
||||
"NYEXX2609110214", null, null, 1)), null);
|
||||
var address = new DomesticWarehouseDirectoryClient.Address("OPERATING", "US", "California", "Rancho Cucamonga",
|
||||
null, "91730", "10767 7th Street", null);
|
||||
var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L,
|
||||
@@ -87,7 +87,7 @@ class DispatchBolPrintServiceTest {
|
||||
new DispatchResponse.BolLine("TSTU3539519", "DP-001", "ABE2", "ABE2 Warehouse",
|
||||
"NYEXX2609110214", null, null, 1),
|
||||
new DispatchResponse.BolLine("TSTU3539519", "DP-002", "ONT8", "ONT8 Warehouse",
|
||||
"NYEXX2609110215", null, null, 1)));
|
||||
"NYEXX2609110215", null, null, 1)), null);
|
||||
var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L,
|
||||
"US-LAX", "洛杉矶仓", "OVERSEAS", "US", "ACTIVE");
|
||||
when(dispatches.platformDetail(detail.dispatchNo())).thenReturn(detail);
|
||||
|
||||
+21
-1
@@ -63,11 +63,13 @@ class DispatchServiceTest {
|
||||
@Mock LastlegOrderMapper tailOrders;
|
||||
@Mock OutboundOrderMapper outboundOrders;
|
||||
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings;
|
||||
@Mock com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow;
|
||||
private DispatchService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new DispatchService();
|
||||
ReflectionTestUtils.setField(service, "fleetWorkflow", fleetWorkflow);
|
||||
ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings);
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "items", items);
|
||||
@@ -435,6 +437,24 @@ class DispatchServiceTest {
|
||||
verify(bolLines).findByDispatchOrder("200", 1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filtersStageBeforePaginationAndPreservesTheServerTotal() {
|
||||
com.baomidou.mybatisplus.core.metadata.TableInfoHelper.initTableInfo(
|
||||
new org.apache.ibatis.builder.MapperBuilderAssistant(new com.baomidou.mybatisplus.core.MybatisConfiguration(), "test"), DispatchOrder.class);
|
||||
Page<DispatchOrder> resultPage = new Page<>(2, 10, 47);
|
||||
resultPage.setRecords(List.of());
|
||||
when(orders.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenAnswer(invocation -> {
|
||||
LambdaQueryWrapper<DispatchOrder> wrapper = invocation.getArgument(1);
|
||||
assertTrue(wrapper.getSqlSegment().contains("status IN"));
|
||||
assertTrue(wrapper.getParamNameValuePairs().values().containsAll(
|
||||
List.of("PICKING", "READY_TO_DEPART", "PARTIAL_READY_TO_DEPART")));
|
||||
return resultPage;
|
||||
});
|
||||
var result = service.platformPage(new DispatchRequests.Query(2, 10, null, null, null, null, DispatchRequests.Stage.LOADING));
|
||||
assertEquals(47, result.total());
|
||||
assertEquals(List.of("DEPARTED", "PARTIAL_DEPARTED"), DispatchRequests.Stage.AFTER_LOADING.statuses());
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformPageQueriesAllCustomersAndLoadsItemsInEachCustomerScope() {
|
||||
DispatchOrder first = new DispatchOrder();
|
||||
@@ -445,7 +465,7 @@ class DispatchServiceTest {
|
||||
resultPage.setRecords(List.of(first, second));
|
||||
when(orders.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(resultPage);
|
||||
|
||||
var result = service.platformPage(new DispatchRequests.Query(1, 20, null, null, null, null));
|
||||
var result = service.platformPage(new DispatchRequests.Query(1, 20, null, null, null, null, null));
|
||||
|
||||
assertEquals(2, result.total());
|
||||
verify(items).findByDispatchOrder("100", 1L);
|
||||
|
||||
+33
-5
@@ -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();
|
||||
|
||||
+33
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user