fix(lastleg): repair dispatch cancellation and return execution contracts
This commit is contained in:
+5
-5
@@ -17,7 +17,7 @@ class FleetBookingFreightTest {
|
||||
final FleetBookingReservations reservations = new FleetBookingReservations(bookings, json);
|
||||
final FleetBookingFreight service = new FleetBookingFreight(bookings, bindings, reservations, json);
|
||||
FleetBookingEntity booking;
|
||||
final FleetDispatchGateway.SourceCargo source = new FleetDispatchGateway.SourceCargo("WT-1", "501", "PLT-501", 12, "FBA:ONT8");
|
||||
final FleetDispatchGateway.SourceCargo source = new FleetDispatchGateway.SourceCargo("IN-1", "501", "PLT-501", 12, "FBA:ONT8");
|
||||
@BeforeEach void setup() throws Exception {
|
||||
booking = new FleetBookingEntity();
|
||||
booking.setId(1L); booking.setTripId(2L); booking.setBookingNo("FB-1"); booking.setIncomingNo("WT-1");
|
||||
@@ -29,10 +29,10 @@ class FleetBookingFreightTest {
|
||||
booking.setReceiptJson(json.writeValueAsString(new FleetBookingModels.AcceptedReceipt(
|
||||
new FleetBookingIncomingGateway.Evidence("WT-1", "IN-1", "WH-1", "FBA:ONT8", "READY_FOR_DISPATCH", "receipt-1",
|
||||
List.of(new FleetBookingIncomingGateway.Pallet("501", "PLT-501", 12, BigDecimal.TEN, BigDecimal.ONE)), 12),
|
||||
List.of(new FleetBookingModels.PalletSpaces("501", 2)), "OP-1", Instant.parse("2026-10-01T00:00:00Z"))));
|
||||
List.of(new FleetBookingModels.PalletSpaces("501", 2, null, null)), "OP-1", Instant.parse("2026-10-01T00:00:00Z"))));
|
||||
var binding = new FleetTripDispatch(); binding.setTripId(2L); binding.setDispatchId(3L);
|
||||
when(bindings.findByDispatchNo("DP-1")).thenReturn(binding);
|
||||
when(bookings.incomingForFreight("WT-1")).thenReturn(booking);
|
||||
when(bookings.forInboundSource(anyString(), eq("IN-1"))).thenReturn(booking);
|
||||
}
|
||||
@Test void quoteUsesConfirmedActualPriceAndSeparatesOtherCargo() {
|
||||
var preview = service.preview(3, "DP-1", "C-1", List.of(source));
|
||||
@@ -53,9 +53,9 @@ class FleetBookingFreightTest {
|
||||
@Test void rejectsPartialOrChangedDestinationAndDetectsNewQuoteVersion() {
|
||||
var before = service.preview(3, "DP-1", "C-1", List.of(source));
|
||||
assertThrows(ResponseStatusException.class, () -> service.preview(3, "DP-1", "C-1", List.of(
|
||||
new FleetDispatchGateway.SourceCargo("WT-1", "501", "PLT-501", 11, "FBA:ONT8"))));
|
||||
new FleetDispatchGateway.SourceCargo("IN-1", "501", "PLT-501", 11, "FBA:ONT8"))));
|
||||
assertThrows(ResponseStatusException.class, () -> service.preview(3, "DP-1", "C-1", List.of(
|
||||
new FleetDispatchGateway.SourceCargo("WT-1", "501", "PLT-501", 12, "FBA:LAX9"))));
|
||||
new FleetDispatchGateway.SourceCargo("IN-1", "501", "PLT-501", 12, "FBA:LAX9"))));
|
||||
booking.setRecordVersion(5L);
|
||||
assertNotEquals(before.snapshotHash(), service.preview(3, "DP-1", "C-1", List.of(source)).snapshotHash());
|
||||
}
|
||||
|
||||
+73
-4
@@ -127,13 +127,18 @@ class FleetTripPersistenceTest {
|
||||
for (String table : List.of("ll_fleet_driver", "ll_fleet_driver_history", "ll_vehicle", "ll_fleet_trip", "ll_fleet_resource_assignment", "ll_fleet_trip_history",
|
||||
"ll_fleet_capacity_offer", "ll_fleet_capacity_offer_command", "ll_fleet_external_booking", "ll_fleet_trip_dispatch", "ll_dispatch_order", "ll_dispatch_pda_task", "ll_outbound_order", "ll_delivery_order",
|
||||
"ll_dispatch_item", "ll_dispatch_pda_pick", "ll_dispatch_pda_operation", "ll_dispatch_pallet_lock", "ll_delivery_operation",
|
||||
"ll_outbound_pallet_item", "ll_exception_order", "ll_exception_return_item", "ll_inventory_ledger", "ll_dispatch_pda_pick_order")) {
|
||||
"ll_outbound_pallet_item", "ll_return_carton_stock_receipt", "ll_exception_order", "ll_exception_return_item", "ll_inventory_ledger", "ll_dispatch_pda_pick_order")) {
|
||||
var match = Pattern.compile("(?s)CREATE TABLE(?: IF NOT EXISTS)? " + table + " \\(.*?;").matcher(schema);
|
||||
assertThat(match.find()).as(table).isTrue();
|
||||
jdbc.execute(match.group().replace(" JSON", " LONGTEXT")
|
||||
jdbc.execute(match.group().replace(" JSON", " LONGTEXT").replaceAll("(?i) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci", "")
|
||||
.replaceAll("\\((receipt_id|return_receipt_id|return_carton_id) IS NOT NULL\\)", "(CASE WHEN $1 IS NOT NULL THEN 1 ELSE 0 END)"));
|
||||
}
|
||||
jdbc.execute("CREATE TABLE ll_warehouse_transfer_order(customer_id VARCHAR(64),transfer_no VARCHAR(64),inbound_code VARCHAR(64))");
|
||||
jdbc.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR(64),pallet_id BIGINT,overseas_warehouse_id VARCHAR(64),balance_id BIGINT,available_packages BIGINT)");
|
||||
jdbc.execute("CREATE TABLE ll_inventory_balance(id BIGINT,customer_id VARCHAR(64),inbound_order_id BIGINT,inbound_code VARCHAR(64))");
|
||||
jdbc.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(64),source_region VARCHAR(32),source_inbound_id VARCHAR(64))");
|
||||
var configuration = new MybatisConfiguration();
|
||||
configuration.addMapper(com.baiyue.lastleg.delivery.mapper.DispatchFeeCargoMapper.class);
|
||||
for (var mapper : List.of(DriverMapper.class, DriverHistoryMapper.class, VehicleMapper.class, FleetTripMapper.class, FleetResourceMapper.class,
|
||||
FleetTripHistoryMapper.class, FleetTripDispatchMapper.class, FleetBookingMapper.class, FleetCapacityOfferMapper.class, FleetCapacityOfferCommandMapper.class, DispatchOrderMapper.class, DispatchPdaTaskMapper.class, OutboundOrderMapper.class, DeliveryOrderMapper.class,
|
||||
DispatchItemMapper.class, DispatchPdaPickMapper.class, DispatchPdaOperationMapper.class, DispatchPalletLockMapper.class, DeliveryOperationMapper.class)) configuration.addMapper(mapper);
|
||||
@@ -161,6 +166,9 @@ class FleetTripPersistenceTest {
|
||||
var bindingMapper = session.getMapper(FleetTripDispatchMapper.class);
|
||||
bindingGuard = transactional(new FleetDispatchBindingGuard(bindingMapper, json), FleetDispatchBindingGuard.class);
|
||||
var workflow = new DispatchPdaWorkflowService();
|
||||
var deliveryExecution = new com.baiyue.lastleg.delivery.service.DeliveryExecutionGuard(new com.baiyue.lastleg.delivery.repository.DeliveryExecutionRepository(
|
||||
session.getMapper(OutboundOrderMapper.class), session.getMapper(DeliveryOrderMapper.class)));
|
||||
ReflectionTestUtils.setField(workflow, "deliveryExecution", deliveryExecution);
|
||||
ReflectionTestUtils.setField(workflow, "orders", session.getMapper(DispatchOrderMapper.class));
|
||||
ReflectionTestUtils.setField(workflow, "outboundOrders", session.getMapper(OutboundOrderMapper.class));
|
||||
ReflectionTestUtils.setField(workflow, "items", session.getMapper(DispatchItemMapper.class));
|
||||
@@ -175,6 +183,9 @@ class FleetTripPersistenceTest {
|
||||
var dispatchGateway = transactional(new DispatchFleetAdapter(session.getMapper(DispatchOrderMapper.class), session.getMapper(DispatchPdaTaskMapper.class),
|
||||
session.getMapper(OutboundOrderMapper.class), session.getMapper(DeliveryOrderMapper.class), session.getMapper(DispatchItemMapper.class), session.getMapper(DispatchPdaPickMapper.class),
|
||||
transactional(workflow, DispatchPdaWorkflowService.class), session.getMapper(DeliveryOperationMapper.class)), DispatchFleetAdapter.class);
|
||||
ReflectionTestUtils.setField(dispatchGateway, "deliveryExecution", deliveryExecution);
|
||||
ReflectionTestUtils.setField(dispatchGateway, "cargoSources", new com.baiyue.lastleg.delivery.service.DispatchCargoSourceQuery(
|
||||
new com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository(session.getMapper(com.baiyue.lastleg.delivery.mapper.DispatchFeeCargoMapper.class))));
|
||||
service = transactional(new FleetTripService(session.getMapper(FleetTripMapper.class), resources, session.getMapper(FleetTripHistoryMapper.class), vehicles, drivers, directory, json,
|
||||
bindingMapper, dispatchGateway, reservations), FleetTripService.class);
|
||||
preparation = transactional(new FleetTripPreparationService(service,session.getMapper(FleetTripMapper.class),session.getMapper(FleetCapacityOfferCommandMapper.class),json),FleetTripPreparationService.class);
|
||||
@@ -530,6 +541,10 @@ class FleetTripPersistenceTest {
|
||||
var confirmed=bookingService.confirm(number,draft.booking().bookingNo(),"1",new FleetBookingModels.Command(draft.tripRecordVersion(),0L,"receipt-reserve",null));
|
||||
var awaiting=new FleetBookingIncomingGateway.Evidence("WT-EXT","WT-EXT","92","FBA:ONT8","WAIT_PDA_RECEIVING","empty",List.of(),0);
|
||||
when(incoming.create(eq("1"),any())).thenReturn(awaiting);
|
||||
jdbc.update("INSERT INTO ll_warehouse_transfer_order VALUES('9','WT-EXT','WT-EXT')");
|
||||
jdbc.update("INSERT INTO ll_inventory_position VALUES(9001,'9',902,'92',9002,22)");
|
||||
jdbc.update("INSERT INTO ll_inventory_balance VALUES(9002,'9',9003,'WT-EXT')");
|
||||
jdbc.update("INSERT INTO ll_order VALUES(9003,'9','WAREHOUSE_TRANSFER',NULL)");
|
||||
var request=new FleetBookingModels.StartIncoming(confirmed.tripRecordVersion(),confirmed.booking().recordVersion(),departure.minusSeconds(120),null,null,null,null,null,"incoming-start");
|
||||
var started=bookingService.startIncoming(number,draft.booking().bookingNo(),"1",request);
|
||||
assertThat(bookingService.startIncoming(number,draft.booking().bookingNo(),"1",request).replayed()).isTrue();
|
||||
@@ -540,11 +555,11 @@ class FleetTripPersistenceTest {
|
||||
when(incoming.inspect("9","WT-EXT",true)).thenReturn(actual);
|
||||
when(incoming.confirm(eq("9"),eq("WT-EXT"),eq("1"),anyString())).thenReturn(actual);
|
||||
assertThatThrownBy(()->bookingService.acceptReceipt(number,draft.booking().bookingNo(),"1",new FleetBookingModels.AcceptReceipt(started.tripRecordVersion(),started.booking().recordVersion(),actual.receiptHash(),
|
||||
List.of(new FleetBookingModels.PalletSpaces("902",1)),new BigDecimal("55.00"),"accept-receipt"))).hasMessageContaining("实收确认未完成");
|
||||
List.of(new FleetBookingModels.PalletSpaces("902",1, null, null)),new BigDecimal("55.00"),"accept-receipt"))).hasMessageContaining("实收确认未完成");
|
||||
assertThat(bookingService.list(number).records().get(0).agreedAmount()).isEqualByComparingTo("100.00");
|
||||
when(incoming.confirm(eq("9"),eq("WT-EXT"),eq("1"),anyString())).thenReturn(new FleetBookingIncomingGateway.Evidence(actual.incomingNo(),actual.inboundCode(),actual.warehouseId(),actual.destinationKey(),"READY_FOR_DISPATCH",actual.receiptHash(),actual.pallets(),actual.receivedPackages()));
|
||||
var accepted=bookingService.acceptReceipt(number,draft.booking().bookingNo(),"1",new FleetBookingModels.AcceptReceipt(started.tripRecordVersion(),started.booking().recordVersion(),actual.receiptHash(),
|
||||
List.of(new FleetBookingModels.PalletSpaces("902",1)),new BigDecimal("55.00"),"accept-receipt"));
|
||||
List.of(new FleetBookingModels.PalletSpaces("902",1, null, null)),new BigDecimal("55.00"),"accept-receipt"));
|
||||
assertThat(accepted.booking().status()).isEqualTo("RECEIVED");assertThat(accepted.booking().cargo().packageCount()).isEqualTo(22);
|
||||
assertThat(accepted.booking().agreedAmount()).isEqualByComparingTo("55.00");
|
||||
assertThat(accepted.booking().quoteSource()).isEqualTo(source);
|
||||
@@ -568,6 +583,33 @@ class FleetTripPersistenceTest {
|
||||
service.unbindDispatch(number,"1",new FleetTripModels.BindingCommand(planned.recordVersion(),"DP-13","撤销未开工关联"));
|
||||
assertThat(bookingService.list(number).records().get(0).status()).isEqualTo("RECEIVED");
|
||||
assertThat(bookingService.list(number).records().get(0).dispatchNo()).isNull();
|
||||
// 解除关联后重新核对已有实收,不创建新库存,也不重复预留承运容量。
|
||||
var reopenedPlan=execution.capacityPlan(number);
|
||||
var refreshed=execution.confirmCapacityPlan(number,"1",new FleetExecutionModels.Confirm(
|
||||
reopenedPlan.recordVersion(),reopenedPlan.manifestHash(),List.of(measurement()),"解除关联后重新核对"));
|
||||
var currentBooking=bookingService.list(number).records().get(0);
|
||||
var receiptAgain=new FleetBookingModels.AcceptReceipt(refreshed.recordVersion(),currentBooking.recordVersion(),actual.receiptHash(),
|
||||
List.of(new FleetBookingModels.PalletSpaces("902",1, null, null)),new BigDecimal("55.00"),"accept-receipt-again");
|
||||
var renewed=bookingService.acceptReceipt(number,currentBooking.bookingNo(),"1",receiptAgain);
|
||||
assertThat(renewed.booking().status()).isEqualTo("RECEIVED");
|
||||
assertThat(renewed.booking().cargo().packageCount()).isEqualTo(22);
|
||||
assertThat(bookingService.acceptReceipt(number,currentBooking.bookingNo(),"1",receiptAgain).replayed()).isTrue();
|
||||
assertThat(execution.capacityPlan(number).availableSegments().get(0).remaining().palletSpaces()).isEqualTo(28);
|
||||
// 实物换托后源摘要变化且旧测量失效;缺测量拒绝,显式实测后更新快照及容量。
|
||||
var moved=new FleetBookingIncomingGateway.Evidence("WT-EXT","WT-EXT","92","FBA:ONT8","READY_FOR_DISPATCH","moved-receipt",
|
||||
List.of(new FleetBookingIncomingGateway.Pallet("903","PALLET-903",22,null,null)),22);
|
||||
when(incoming.inspect("9","WT-EXT",true)).thenReturn(moved);
|
||||
when(incoming.confirm(eq("9"),eq("WT-EXT"),eq("1"),anyString())).thenReturn(moved);
|
||||
var withoutMeasure=new FleetBookingModels.AcceptReceipt(renewed.tripRecordVersion(),renewed.booking().recordVersion(),moved.receiptHash(),
|
||||
List.of(new FleetBookingModels.PalletSpaces("903",1,null,null)),new BigDecimal("55.00"),"moved-confirm");
|
||||
assertThatThrownBy(()->bookingService.acceptReceipt(number,currentBooking.bookingNo(),"1",withoutMeasure)).hasMessageContaining("实际托盘");
|
||||
var withMeasure=new FleetBookingModels.AcceptReceipt(renewed.tripRecordVersion(),renewed.booking().recordVersion(),moved.receiptHash(),
|
||||
List.of(new FleetBookingModels.PalletSpaces("903",1,new BigDecimal("170"),new BigDecimal("1.7"))),new BigDecimal("55.00"),"moved-confirm");
|
||||
var measuredAgain=bookingService.acceptReceipt(number,currentBooking.bookingNo(),"1",withMeasure);
|
||||
assertThat(measuredAgain.booking().cargo().weightKg()).isEqualByComparingTo("170");
|
||||
assertThat(measuredAgain.booking().cargo().volumeCbm()).isEqualByComparingTo("1.7");
|
||||
assertThat(measuredAgain.booking().receipt().source().pallets()).singleElement().satisfies(p->{assertThat(p.warehousePalletId()).isEqualTo("903");assertThat(p.weightKg()).isEqualByComparingTo("170");});
|
||||
assertThat(bookingService.acceptReceipt(number,currentBooking.bookingNo(),"1",withMeasure).replayed()).isTrue();
|
||||
}
|
||||
|
||||
@Test void externalInquiryDoesNotReserveUntilConfirmedAndCancellationReleasesExactlyOnce() {
|
||||
@@ -931,6 +973,31 @@ class FleetTripPersistenceTest {
|
||||
} finally { executor.shutdownNow(); }
|
||||
}
|
||||
|
||||
/** 取消与实际整车发车并发时,已拣货车只能发车;出库和派送不得产生取消终态。 */
|
||||
@Test void outboundCancellationRacingDepartureCannotCreateDepartedCancelledCargo() throws Exception {
|
||||
String number=readyTrip();var loaded=confirm(number);
|
||||
var cancel=new com.baiyue.lastleg.delivery.service.DeliveryTransferService();
|
||||
var config=new MybatisConfiguration();config.addMapper(OutboundOrderMapper.class);config.addMapper(DispatchOrderMapper.class);
|
||||
var source=(javax.sql.DataSource)jdbc.getDataSource();
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(source);factory.setConfiguration(config);factory.afterPropertiesSet();
|
||||
var data=new SqlSessionTemplate(factory.getObject());
|
||||
ReflectionTestUtils.setField(cancel,"outboundOrders",data.getMapper(OutboundOrderMapper.class));
|
||||
ReflectionTestUtils.setField(cancel,"dispatchOrders",data.getMapper(DispatchOrderMapper.class));
|
||||
ReflectionTestUtils.setField(cancel,"fleetBindings",bindingGuard);
|
||||
var start=new CountDownLatch(1);var executor=Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
var departure=executor.submit(()->{start.await(5,TimeUnit.SECONDS);return execution.depart("92","DP-11","1",new FleetExecutionModels.Depart(loaded.recordVersion(),"race-depart","并发取消验证"));});
|
||||
var cancellation=executor.submit(()->{start.await(5,TimeUnit.SECONDS);try{
|
||||
new org.springframework.transaction.support.TransactionTemplate(transactions).executeWithoutResult(s->cancel.cancelOutbound("OUT-11","1",new com.baiyue.lastleg.delivery.dto.DeliveryRequests.Cancel("并发取消")));
|
||||
return false;
|
||||
}catch(ResponseStatusException rejected){return rejected.getStatusCode().value()==409;}});
|
||||
start.countDown();assertThat(departure.get(20,TimeUnit.SECONDS).shippedPackages()).isEqualTo(20);assertThat(cancellation.get(20,TimeUnit.SECONDS)).isTrue();
|
||||
assertThat(jdbc.queryForList("SELECT status FROM ll_outbound_order",String.class)).containsOnly("OUTBOUNDED");
|
||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_delivery_order WHERE status='CANCELLED'",Integer.class)).isZero();
|
||||
assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pda_operation WHERE operation_type='DEPART'",Integer.class)).isEqualTo(2);
|
||||
} finally {executor.shutdownNow();}
|
||||
}
|
||||
|
||||
@Test void changingAssignedDriverToDomesticBlocksPreviewAndDepartureUntilCorrected() {
|
||||
String number = readyTrip(); var confirmed = confirm(number);
|
||||
long driverId = Long.parseLong(service.detail(number).trip().driverId());
|
||||
@@ -1125,6 +1192,8 @@ class FleetTripPersistenceTest {
|
||||
dispatchId + 1000, customer, "OUT-" + dispatchId, dispatchId, "out-key-" + dispatchId);
|
||||
jdbc.update("INSERT INTO ll_delivery_order(id,customer_id,delivery_no,outbound_order_id,outbound_no,dispatch_order_id,delivery_group_key,idempotency_key,expected_arrival_at) VALUES(?,?,?,?,?,?,?,?,?)",
|
||||
dispatchId + 2000, customer, "DL-" + dispatchId, dispatchId + 1000, "OUT-" + dispatchId, dispatchId, "DEST", "dl-key-" + dispatchId, LocalDateTime.of(2030, 1, 1, 12, 0));
|
||||
jdbc.update("UPDATE ll_outbound_order SET package_count=10 WHERE id=?", dispatchId + 1000);
|
||||
jdbc.update("UPDATE ll_delivery_order SET package_count=10 WHERE id=?", dispatchId + 2000);
|
||||
}
|
||||
|
||||
private boolean attempt(CountDownLatch start, String number, long vehicle, long driver) throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user