diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java index 1fbb530f4..ad135437d 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java @@ -123,7 +123,7 @@ class PlatformDispatchOutboundContractTest { assertTrue(Arrays.stream(DispatchController.class.getDeclaredMethods()) .noneMatch(method -> Set.of("depart", "cancel").contains(method.getName()))); assertEquals(Set.of("recordVersion", "idempotencyKey", "vehicle", "driver", - "plannedDepartureAt", "estimatedArrivalAt", "remark", "publishLooseboardDemand"), + "plannedDepartureAt", "estimatedArrivalAt", "remark", "publishLooseboardDemand", "maintenance"), Arrays.stream(DispatchCommands.Update.class.getRecordComponents()) .map(component -> component.getName()).collect(java.util.stream.Collectors.toSet())); assertEquals(Set.of("recordVersion", "idempotencyKey", "reason"), diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/AppointmentWorkPreArrangementPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/AppointmentWorkPreArrangementPersistenceTest.java index 79774d549..726527942 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/AppointmentWorkPreArrangementPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/AppointmentWorkPreArrangementPersistenceTest.java @@ -20,7 +20,7 @@ import static org.mockito.Mockito.*; import static com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.*; class AppointmentWorkPreArrangementPersistenceTest { - @Test void siblingPlanChangeNeverAuthorizesUnconfirmedFbaCargoToEnterDispatch() { + @Test void siblingPlanChangePreservesPrearrangementButNeverAuthorizesUnpickedDeparture() { try(var context=new AnnotationConfigApplicationContext(FbaAppointmentPersistenceTest.Config.class)) { var sources=context.getBean(AppointmentSourceService.class); var cargo=List.of(source("B",1,60),source("D",2,40)); @@ -45,15 +45,18 @@ class AppointmentWorkPreArrangementPersistenceTest { var tx=new TransactionTemplate(context.getBean(PlatformTransactionManager.class)); var before=candidates.read(batch.batchKey()).candidates().stream().filter(c->c.customerId().equals("B")).findFirst().orElseThrow(); assertFalse(before.blockReasons().isEmpty());assertEquals(60,before.packageCount()); - assertNotBindable(tx,binding,mapper,before); + assertPrearrangementAllowed(tx,binding,mapper,before); batch=receiving.plan("operator",new Plan("pending-d",new Selection(batch.batchKey(),batch.version(),"D"),plan("PENDING"),"transport-v1")).batches().get(0); assertEquals("PENDING",batch.appointmentStatus());assertEquals(originalB,store.plan(batch.batchKey(),"B")); var after=candidates.read(batch.batchKey()).candidates().stream().filter(c->c.customerId().equals("B")).findFirst().orElseThrow(); assertEquals(before.packageCount(),after.packageCount());assertEquals(before.blockReasons(),after.blockReasons()); - assertNotEquals(before.allocations().get(0).recordVersion(),after.allocations().get(0).recordVersion());assertNotBindable(tx,binding,mapper,after); + assertNotEquals(before.allocations().get(0).recordVersion(),after.allocations().get(0).recordVersion());assertPrearrangementAllowed(tx,binding,mapper,after); var bound=new AppointmentWorkDispatchMapper.Bound(20L,201L,101L,batch.batchKey(),"B","ORDER:1","FBA:ONT8",60); doReturn(List.of(bound)).when(mapper).bindings("DP-B"); - assertTrue(assertThrows(ResponseStatusException.class,()->binding.requireDeparture("DP-B")).getReason().contains("尚无有效收货确认")); + // 预约号门槛归整车审核;来源端口不伪造正式确认,且预排不能替代真实拣货。 + assertTrue(binding.reviewEvidence("DP-B").isEmpty()); + doReturn(List.of()).when(mapper).pickedPhysical(201L,"B"); + assertTrue(assertThrows(ResponseStatusException.class,()->binding.requireDeparture("DP-B")).getReason().contains("缺少真实逐票分配")); // Unchanged real-source guards still reject an unverified pallet and an allocation owned by another customer. doReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"B","FBA:ONT8",60,null))).when(mapper).physical(101L,"B"); assertFalse(candidates.read(batch.batchKey()).candidates().get(0).blockReasons().isEmpty()); @@ -65,11 +68,16 @@ class AppointmentWorkPreArrangementPersistenceTest { assertTrue(mapper.byDispatch(20).isEmpty()); } } - private static void assertNotBindable(TransactionTemplate tx,AppointmentWorkDispatchBinding binding,AppointmentWorkDispatchMapper mapper,AppointmentWorkDispatchCandidates.Candidate candidate) { - assertThrows(ResponseStatusException.class,()->tx.executeWithoutResult(status->{ + /** 最新契约允许先排后约,但真实来源、数量及发车确认仍独立复验;回滚本段以隔离后续负例。 */ + private static void assertPrearrangementAllowed(TransactionTemplate tx,AppointmentWorkDispatchBinding binding,AppointmentWorkDispatchMapper mapper,AppointmentWorkDispatchCandidates.Candidate candidate) { + tx.executeWithoutResult(status->{ binding.lockBatches(Map.of(candidate.allocations().get(0).batchKey(),candidate.allocations().get(0).recordVersion())); binding.bindItems(20,List.of(new AppointmentWorkDispatchPort.PhysicalItem(201L,"B",101L,"RECEIVED",60)),candidate.allocations()); - })); + var allocated=mapper.byDispatch(20); + assertEquals(1,allocated.size());assertEquals("B",allocated.get(0).customerId()); + assertEquals(60,allocated.get(0).packages());assertEquals("ORDER:1",allocated.get(0).sourceKey()); + status.setRollbackOnly(); + }); assertTrue(mapper.byDispatch(20).isEmpty()); } private static FbaAppointmentDetails plan(String status) {return new FbaAppointmentDetails(LocalDate.of(2026,10,25),null,null,null,null,null,status,"仓库暂无确认",null,null);}