feat(lastleg): connect appointment followups and fleet intake

This commit is contained in:
2026-10-09 22:04:49 +08:00
parent 1202cafadf
commit 0d75e311d0
54 changed files with 2315 additions and 25 deletions
@@ -0,0 +1,32 @@
package com.baiyue.lastleg.dispatch.fleet;
import com.baiyue.lastleg.dispatch.service.DispatchService;
import com.baiyue.lastleg.fleet.trip.*;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
class FleetDispatchPlanningAdapterTest {
private final FleetDispatchPlanningMapper mapper=mock(FleetDispatchPlanningMapper.class);
private final DispatchService service=mock(DispatchService.class);
private final FleetDispatchGateway gateway=mock(FleetDispatchGateway.class);
private final FleetDispatchPlanningAdapter adapter=new FleetDispatchPlanningAdapter(mapper,service,gateway);
private FleetBookingIncomingGateway.Evidence receipt(){return new FleetBookingIncomingGateway.Evidence("WT-1","IN-1","92","FBA:ACY2","READY_FOR_DISPATCH","hash",List.of(new FleetBookingIncomingGateway.Pallet("1","P-1",10,BigDecimal.TEN,BigDecimal.ONE)),10);}
@Test void refusesAmbiguousSourcesWithoutCreatingAnotherDispatch() {
when(mapper.sourceDispatches("9","IN-1")).thenReturn(List.of("DS-1","DS-2"));
assertThatThrownBy(()->adapter.resolveIncoming("9",receipt(),"1")).hasMessageContaining("多张排车");
verifyNoInteractions(service);
}
@Test void reusesOnlyAnExactExistingShipment() {
when(mapper.sourceDispatches("9","IN-1")).thenReturn(List.of("DS-1"));
when(gateway.lockForPlanning("DS-1")).thenReturn(new FleetDispatchGateway.Snapshot(1,"DS-1","9","92",10));
when(gateway.sourceCargo("DS-1")).thenReturn(List.of(new FleetDispatchGateway.SourceCargo("IN-1","1","P-1",10,"FBA:ACY2")));
assertThat(adapter.resolveIncoming("9",receipt(),"1")).isEqualTo("DS-1");verifyNoInteractions(service);
when(gateway.sourceCargo("DS-1")).thenReturn(List.of(new FleetDispatchGateway.SourceCargo("IN-1","1","P-1",9,"FBA:ACY2")));
assertThatThrownBy(()->adapter.resolveIncoming("9",receipt(),"1")).hasMessageContaining("全部实收");
when(gateway.sourceCargo("DS-1")).thenReturn(List.of(new FleetDispatchGateway.SourceCargo("IN-other","1","P-1",10,"FBA:ACY2")));
assertThatThrownBy(()->adapter.resolveIncoming("9",receipt(),"1")).hasMessageContaining("其他来源");
}
}
@@ -0,0 +1,51 @@
package com.baiyue.lastleg.dispatch.fleet;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.lastleg.fleet.business.*;
import com.fasterxml.jackson.databind.ObjectMapper;
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.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.*;
class FleetPlanningReadPersistenceTest {
@Test void candidatesPaginateOnlyEligibleSameWarehouseUnboundCargoAndSourcesPreserveUnknowns() 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_dispatch_order(id BIGINT PRIMARY KEY,dispatch_no VARCHAR(64),customer_id VARCHAR(64),overseas_warehouse_id VARCHAR(64),package_count INT,status VARCHAR(32),picked_package_count INT,shipped_package_count INT,updated_at TIMESTAMP,route VARCHAR(500))");
jdbc.execute("CREATE TABLE ll_dispatch_item(dispatch_order_id BIGINT,customer_id VARCHAR(64),inbound_code VARCHAR(64))");
jdbc.execute("CREATE TABLE ll_fleet_trip_dispatch(dispatch_id BIGINT)");
jdbc.execute("CREATE TABLE ll_dispatch_pda_task(dispatch_order_id BIGINT,status VARCHAR(32))");
jdbc.execute("CREATE TABLE ll_outbound_order(dispatch_order_id BIGINT,status VARCHAR(32))");
jdbc.execute("CREATE TABLE ll_fleet_trip(id BIGINT PRIMARY KEY,warehouse_id BIGINT)");
jdbc.execute("CREATE TABLE ll_fleet_external_booking(id BIGINT,trip_id BIGINT,customer_id VARCHAR(64),booking_no VARCHAR(64),trip_no VARCHAR(64),status VARCHAR(32),offer_id BIGINT,destination_key VARCHAR(160),destination_label VARCHAR(100),incoming_no VARCHAR(64),record_version BIGINT,cargo_json VARCHAR(2000),expected_pallet_count INT,estimated_arrival_at TIMESTAMP)");
jdbc.execute("CREATE TABLE ll_fleet_pool_order_link(booking_id BIGINT,customer_id VARCHAR(64),lastleg_order_id BIGINT,business_order_no VARCHAR(64),fulfillment_stage VARCHAR(32))");
for(int i=1;i<=6;i++)jdbc.update("INSERT INTO ll_dispatch_order(id,dispatch_no,customer_id,overseas_warehouse_id,package_count,status,picked_package_count,shipped_package_count,updated_at) VALUES(?,?,?,'92',10,'PENDING_DEPARTURE',0,0,CURRENT_TIMESTAMP)",i,"DS-"+i,"9");
jdbc.update("INSERT INTO ll_fleet_trip_dispatch VALUES(2)");
jdbc.update("UPDATE ll_dispatch_order SET overseas_warehouse_id='93' WHERE id=3");
jdbc.update("INSERT INTO ll_dispatch_pda_task VALUES(4,'PICKING')");
jdbc.update("INSERT INTO ll_dispatch_item VALUES(5,'9','IN-5'),(6,'9','IN-6')");
jdbc.update("INSERT INTO ll_fleet_trip VALUES(1,92)");
jdbc.update("INSERT INTO ll_fleet_external_booking(id,trip_id,customer_id,booking_no,trip_no,status,offer_id,destination_key,destination_label,incoming_no,record_version,cargo_json) VALUES(5,1,'9','FB-5','TR-OTHER','RECEIVED',1,'FBA:ACY2','ACY2','IN-5',1,'{\"packageCount\":null}'),(6,1,'9','FB-6','TR-1','RECEIVED',1,'FBA:ACY2','ACY2','IN-6',2,'{\"packageCount\":10}')");
var configuration=new MybatisConfiguration();configuration.addMapper(FleetDispatchPlanningMapper.class);configuration.addMapper(FleetPoolOrderMapper.class);
var interceptor=new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(com.baomidou.mybatisplus.annotation.DbType.H2));configuration.addInterceptor(interceptor);
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(configuration);
var session=new SqlSessionTemplate(factory.getObject());var mapper=session.getMapper(FleetDispatchPlanningMapper.class);
var page=mapper.candidates(new Page<>(1,1),"TR-1","92",null);
assertThat(page.getTotal()).isEqualTo(2);assertThat(page.getRecords()).hasSize(1);
assertThat(mapper.candidates(new Page<>(1,100),"TR-1","92","DS-1").getRecords()).extracting(r->r.dispatchNo()).containsExactly("DS-1");
var resolver=new FleetPoolSourceResolver(session.getMapper(FleetPoolOrderMapper.class),new ObjectMapper());
var sources=resolver.resolve(List.of(new FleetPoolSourceResolver.SourceRef("9","FB-5"),new FleetPoolSourceResolver.SourceRef("9","FB-6"),new FleetPoolSourceResolver.SourceRef("OTHER","FB-6")));
assertThat(sources).hasSize(2);
assertThat(sources.stream().filter(s->s.bookingNo().equals("FB-5")).findFirst().orElseThrow().packageCount()).isNull();
assertThat(sources.stream().filter(s->s.bookingNo().equals("FB-6")).findFirst().orElseThrow().packageCount()).isEqualTo(10);
assertThat(sources).allMatch(s->s.lastlegOrderId()==null&&s.expectedPalletCount()==null);
}
}