feat(fleet): expose bounded origin-aware trip calendar
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
package com.baiyue.lastleg.fleet.trip;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class FleetTripCalendarMapperTest {
|
||||
@Test void queriesAHalfOpenUtcWindowWithoutStatusFilteringAndCapsCandidateRows() throws Exception {
|
||||
var ds=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");
|
||||
var jdbc=new JdbcTemplate(ds);
|
||||
jdbc.execute("CREATE TABLE ll_fleet_trip(trip_no VARCHAR(64) PRIMARY KEY,status VARCHAR(32),warehouse_id BIGINT,warehouse_code VARCHAR(64),warehouse_name VARCHAR(100),planned_departure_at TIMESTAMP,expected_driver_end_at TIMESTAMP,departed_at TIMESTAMP,completed_at TIMESTAMP,stops_json VARCHAR(2000))");
|
||||
var from=LocalDateTime.of(2026,11,1,0,0);var to=from.plusDays(1);
|
||||
String insert="INSERT INTO ll_fleet_trip(trip_no,status,warehouse_id,planned_departure_at,expected_driver_end_at,stops_json) VALUES(?,?,1,?,?, '[]')";
|
||||
jdbc.update(insert,"TR-BEFORE","PLANNED",from.minusSeconds(1),to);
|
||||
jdbc.update(insert,"TR-FROM","CANCELLED",from,to);
|
||||
jdbc.update(insert,"TR-LAST","COMPLETED",to.minusSeconds(1),to);
|
||||
jdbc.update(insert,"TR-TO","IN_TRANSIT",to,to.plusDays(1));
|
||||
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.addMapper(FleetTripMapper.class);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);
|
||||
var mapper=new SqlSessionTemplate(factory.getObject()).getMapper(FleetTripMapper.class);
|
||||
var rows=mapper.calendar(from,to);
|
||||
assertThat(rows).extracting(FleetTripEntity::getTripNo).containsExactly("TR-FROM","TR-LAST");
|
||||
assertThat(rows).extracting(FleetTripEntity::getStatus).containsExactly("CANCELLED","COMPLETED");
|
||||
assertThat(rows.get(0).getPlannedDepartureAt()).isEqualTo(from);
|
||||
var many=new ArrayList<Object[]>();for(int i=0;i<2002;i++)many.add(new Object[]{"TR-MANY-"+i,"PLANNED",from.plusHours(1),to});
|
||||
jdbc.batchUpdate(insert,many);
|
||||
assertThat(mapper.calendar(from,to)).hasSize(2001);
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package com.baiyue.lastleg.fleet.trip;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.lastleg.fleet.driver.FleetWarehouseClient;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class FleetTripCalendarServiceTest {
|
||||
final FleetTripMapper trips=mock(FleetTripMapper.class);
|
||||
final FleetWarehouseClient warehouses=mock(FleetWarehouseClient.class);
|
||||
final FleetTripCalendarService service=new FleetTripCalendarService(trips,warehouses,new ObjectMapper(),"internal-test");
|
||||
final Instant from=Instant.parse("2026-10-01T00:00:00Z"),to=from.plus(Duration.ofDays(44));
|
||||
FleetTripEntity trip(String no,Long warehouse) {
|
||||
var row=new FleetTripEntity();row.setTripNo(no);row.setWarehouseId(warehouse);row.setStatus("PLANNED");
|
||||
row.setPlannedDepartureAt(LocalDateTime.ofInstant(from,ZoneOffset.UTC));
|
||||
row.setExpectedDriverEndAt(row.getPlannedDepartureAt().plusDays(2));
|
||||
row.setStopsJson("[{\"name\":\"Origin\"},{\"name\":\"ACY2\",\"destinationKey\":\"FBA:ACY2\"}]");return row;
|
||||
}
|
||||
FleetWarehouseClient.Metadata warehouse(long id,String country,String timezone) {
|
||||
return new FleetWarehouseClient.Metadata(id,"W-"+id,"Warehouse "+id,"OVERSEAS",country,timezone,"INACTIVE");
|
||||
}
|
||||
void metadata(List<FleetWarehouseClient.Metadata> records,List<Long> missing) {
|
||||
when(warehouses.platformMetadata(anyString(),any())).thenReturn(ApiResponse.success(new FleetWarehouseClient.MetadataResult(records,missing)));
|
||||
}
|
||||
@Test void validatesProtectiveUtcWindowAndDoesNotFetchMetadataForAnEmptyCalendar() {
|
||||
assertThatThrownBy(()->service.calendar(null,to)).isInstanceOf(ResponseStatusException.class);
|
||||
assertThatThrownBy(()->service.calendar(from,from)).isInstanceOf(ResponseStatusException.class);
|
||||
assertThatThrownBy(()->service.calendar(to,from)).isInstanceOf(ResponseStatusException.class);
|
||||
assertThatThrownBy(()->service.calendar(from,to.plusNanos(1))).isInstanceOf(ResponseStatusException.class);
|
||||
verifyNoInteractions(trips,warehouses);
|
||||
when(trips.calendar(any(),any())).thenReturn(List.of());
|
||||
assertThat(service.calendar(from,to).records()).isEmpty();
|
||||
verify(trips).calendar(LocalDateTime.of(2026,10,1,0,0),LocalDateTime.of(2026,11,14,0,0));
|
||||
verifyNoInteractions(warehouses);
|
||||
}
|
||||
@Test void preservesUtcFactsAndInactiveOriginsWhileSeparatingUnknownSourcesFromUsSlots() {
|
||||
var us=trip("TR-US",1L);us.setStatus("CANCELLED");us.setDepartedAt(us.getPlannedDepartureAt().plusHours(1));
|
||||
when(trips.calendar(any(),any())).thenReturn(List.of(us,trip("TR-CA",2L),trip("TR-UTC",3L),trip("TR-COUNTRY",4L),trip("TR-MISSING",5L),trip("TR-NO-ORIGIN",null)));
|
||||
metadata(List.of(warehouse(1,"US","America/Los_Angeles"),warehouse(2,"CA","America/Toronto"),warehouse(3,"US","UTC"),warehouse(4,null,"America/New_York")),List.of(5L));
|
||||
var result=service.calendar(from,to);
|
||||
assertThat(result.records()).singleElement().satisfies(row->{
|
||||
assertThat(row.sourceType()).isEqualTo("TRIP_PLAN");assertThat(row.tripNo()).isEqualTo("TR-US");
|
||||
assertThat(row.status()).isEqualTo("CANCELLED");assertThat(row.originWarehouseStatus()).isEqualTo("INACTIVE");
|
||||
assertThat(row.plannedDepartureAt()).isEqualTo(from);assertThat(row.departedAt()).isEqualTo(from.plusSeconds(3600));
|
||||
assertThat(row.expectedDriverEndAt()).isEqualTo(from.plus(Duration.ofDays(2)));assertThat(row.completedAt()).isNull();
|
||||
assertThat(row.stops()).hasSize(2);assertThat(row.incompleteReasons()).isEmpty();
|
||||
});
|
||||
assertThat(result.unresolvedRecords()).extracting(FleetTripCalendarService.Entry::tripNo).containsExactly("TR-UTC","TR-COUNTRY","TR-MISSING","TR-NO-ORIGIN");
|
||||
assertThat(result.unresolvedRecords().get(0).originTimezone()).isNull();
|
||||
assertThat(result.unresolvedRecords().get(0).incompleteReasons()).containsExactly("ORIGIN_TIMEZONE_UNKNOWN");
|
||||
assertThat(result.unresolvedRecords().get(2).incompleteReasons()).contains("ORIGIN_WAREHOUSE_UNKNOWN");
|
||||
verify(warehouses).platformMetadata("internal-test",new FleetWarehouseClient.MetadataRequest(List.of(1L,2L,3L,4L,5L)));
|
||||
verifyNoMoreInteractions(warehouses);
|
||||
}
|
||||
@Test void sharesOneMetadataReadAndFlagsMalformedRoutesWithoutInventingStops() {
|
||||
var broken=trip("TR-BROKEN",1L);broken.setStopsJson("invalid");
|
||||
when(trips.calendar(any(),any())).thenReturn(List.of(trip("TR-1",1L),broken));metadata(List.of(warehouse(1,"US","America/New_York")),List.of());
|
||||
var result=service.calendar(from,to);assertThat(result.records()).hasSize(2);
|
||||
assertThat(result.records().get(1).stops()).isEmpty();assertThat(result.records().get(1).incompleteReasons()).containsExactly("ROUTE_INCOMPLETE");
|
||||
verify(warehouses).platformMetadata("internal-test",new FleetWarehouseClient.MetadataRequest(List.of(1L)));verifyNoMoreInteractions(warehouses);
|
||||
}
|
||||
@Test void rejectsOverflowRatherThanReturningAPartialCalendar() {
|
||||
when(trips.calendar(any(),any())).thenReturn(Collections.nCopies(2001,trip("TR-1",1L)));
|
||||
assertThatThrownBy(()->service.calendar(from,to)).isInstanceOf(ResponseStatusException.class).hasMessageContaining("缩小查询日期");verifyNoInteractions(warehouses);
|
||||
}
|
||||
@Test void metadataFailureOrIncompleteProtocolCannotMasqueradeAsAnEmptyUsCalendar() {
|
||||
when(trips.calendar(any(),any())).thenReturn(List.of(trip("TR-1",1L)));
|
||||
metadata(List.of(),List.of());assertThatThrownBy(()->service.calendar(from,to)).isInstanceOf(ResponseStatusException.class).hasMessageContaining("仓库时区资料暂不可用");
|
||||
metadata(List.of(warehouse(1,"US","America/New_York"),warehouse(1,"US","America/New_York")),List.of());
|
||||
assertThatThrownBy(()->service.calendar(from,to)).isInstanceOf(ResponseStatusException.class);
|
||||
when(warehouses.platformMetadata(anyString(),any())).thenThrow(new IllegalStateException("offline"));
|
||||
assertThatThrownBy(()->service.calendar(from,to)).isInstanceOf(ResponseStatusException.class);
|
||||
}
|
||||
@Test void endpointUsesExistingReadPermission() throws Exception {
|
||||
var method=PlatformFleetTripController.class.getMethod("calendar",com.baiyue.common.security.AuthenticatedUser.class,Instant.class,Instant.class);
|
||||
assertThat(method.getAnnotation(org.springframework.security.access.prepost.PreAuthorize.class).value()).isEqualTo("hasAuthority('platform:fleet:trip:list')");
|
||||
}
|
||||
@Test void systemVAndFixedOffsetsAreNotGeographicOriginTimezones() {
|
||||
when(trips.calendar(any(),any())).thenReturn(List.of(trip("TR-1",1L)));
|
||||
for (String zone:List.of("SystemV/EST5EDT","Etc/GMT+5","+08:00","UTC","not-a-zone")) {
|
||||
metadata(List.of(warehouse(1,"US",zone)),List.of());
|
||||
var result=service.calendar(from,to);
|
||||
assertThat(result.records()).isEmpty();
|
||||
assertThat(result.unresolvedRecords()).singleElement().satisfies(row->{
|
||||
assertThat(row.originTimezone()).isNull();
|
||||
assertThat(row.incompleteReasons()).containsExactly("ORIGIN_TIMEZONE_UNKNOWN");
|
||||
});
|
||||
}
|
||||
}
|
||||
@Test void nonPlatformSubjectsAreRejectedEvenWhenTheyHaveTheReadPermission() {
|
||||
var controller=new PlatformFleetTripController(mock(FleetTripService.class),mock(FleetTripExecutionService.class),mock(FleetTripCompletionService.class));
|
||||
var calendar=mock(FleetTripCalendarService.class);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(controller,"calendar",calendar);
|
||||
assertThatThrownBy(()->controller.calendar(null,from,to)).isInstanceOf(ResponseStatusException.class);
|
||||
for (var user:List.of(
|
||||
new com.baiyue.common.security.AuthenticatedUser(1L,0L,0L,1L,"client","MERCHANT","WEB",Set.of("platform:fleet:trip:list")),
|
||||
new com.baiyue.common.security.AuthenticatedUser(1L,9L,9L,1L,"client","PLATFORM","WEB",Set.of("platform:fleet:trip:list")),
|
||||
new com.baiyue.common.security.AuthenticatedUser(1L,null,0L,1L,"client","PLATFORM","WEB",Set.of("platform:fleet:trip:list")),
|
||||
new com.baiyue.common.security.AuthenticatedUser(null,0L,0L,1L,"client","PLATFORM","WEB",Set.of("platform:fleet:trip:list"))))
|
||||
assertThatThrownBy(()->controller.calendar(user,from,to)).isInstanceOf(ResponseStatusException.class);
|
||||
verifyNoInteractions(calendar);
|
||||
var platform=new com.baiyue.common.security.AuthenticatedUser(1L,0L,0L,1L,"platform","PLATFORM","WEB",Set.of("platform:fleet:trip:list"));
|
||||
controller.calendar(platform,from,to);verify(calendar).calendar(from,to);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user