fix(lastleg): preserve source validation during partial dispatch picking

This commit is contained in:
2026-10-23 04:42:16 +08:00
parent f946285bcc
commit f6c83125c9
2 changed files with 33 additions and 3 deletions
@@ -59,14 +59,16 @@ class AppointmentWorkDispatchBindingTest {
when(sources.canonicalOrders(anyList())).thenReturn(List.of(new AppointmentCoverageService.SourceRef("C","ORDER:1",null)));
assertDoesNotThrow(()->service.requireDeparture("D-20"));
when(mapper.receivedPhysical(10,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",null,4)));
when(mapper.pickedPhysical(30,"C")).thenReturn(List.of());
var departure=java.time.Instant.parse("2026-10-18T12:00:00Z");
var arrival=java.time.Instant.parse("2026-10-18T17:00:00Z");
clearInvocations(mapper);
assertNull(service.reviewBlockReason("D-20",departure,arrival));
verify(mapper,never()).pickedPhysical(anyLong(),anyString());
verify(mapper).pickedPhysical(30,"C");
assertEquals(arrival,service.reviewEvidence("D-20").get(0).startAt(),"10点洛杉矶预约应按当地时区解释");
// 目的仓编号及时间由整车入口校验;此端口只核实真实来源分配。
assertNull(service.reviewBlockReason("D-20",departure,null));
when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",null,4)));
when(item.packageCount()).thenReturn(4);when(mapper.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",20)).thenReturn(1L);
assertTrue(service.departureBlockReason("D-20").contains("分配量"));
when(item.packageCount()).thenReturn(4);when(mapper.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",20)).thenReturn(0L);
@@ -85,6 +87,7 @@ class AppointmentWorkDispatchBindingTest {
assertDoesNotThrow(()->service.requireDeparture("D-20"));
when(receiving.currentAgreement("FBA_APPOINTMENT:1","C")).thenReturn(null);
assertDoesNotThrow(()->service.requireDeparture("D-20"));
when(mapper.receivedPhysical(10,"C")).thenReturn(List.of());
assertNull(service.reviewBlockReason("D-20",departure,arrival));
verifyNoInteractions(changes);
}
@@ -122,6 +125,31 @@ class AppointmentWorkDispatchBindingTest {
when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","CUSTOMER:1",null,4)));
assertDoesNotThrow(()->service.requireDeparture("D-20"));verifyNoInteractions(appointments);
}
@Test void partialPickingKeepsReviewEligibleButDepartureStillRequiresAllOriginalSources() {
var mapper=mock(AppointmentWorkDispatchMapper.class);var sources=mock(AppointmentSourceService.class);
var service=new AppointmentWorkDispatchBinding(mapper,mock(InboundFbaAppointmentService.class),sources,mock(AppointmentWorkChanges.class),mock(com.baiyue.lastleg.inboundplan.receiving.FbaReceivingStore.class),mock(AppointmentWorkDispatchCandidates.class));
when(mapper.dispatchPhysical("D")).thenReturn(List.of(new AppointmentWorkDispatchMapper.DispatchPhysical(30,10L,"C","RECEIVED",4)));
when(mapper.bindings("D")).thenReturn(List.of(
new AppointmentWorkDispatchMapper.Bound(20,30,10,"DISPATCH:20","C","ORDER:1","FBA:A",1),
new AppointmentWorkDispatchMapper.Bound(20,30,10,"DISPATCH:20","C","ORDER:2","FBA:A",3)));
when(sources.canonicalOrders(anyList())).thenAnswer(call->((List<AppointmentExternalSourceGateway.OrderRef>)call.getArgument(0)).stream()
.map(ref->new AppointmentCoverageService.SourceRef(ref.customerId(),"ORDER:"+ref.orderId(),null)).toList());
var first=new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:A",null,1);
var second=new AppointmentWorkDispatchMapper.Physical(2L,"C","FBA:A",null,3);
when(mapper.receivedPhysical(10,"C")).thenReturn(List.of(second));
when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(first));
assertNull(service.reviewBlockReason("D",null,null));
assertNotNull(service.departureBlockReason("D"));
when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(3L,"C","FBA:A",null,1)));
assertNotNull(service.reviewBlockReason("D",null,null),"同箱数的其他来源不能补足原分配");
when(mapper.pickedPhysical(30,"C")).thenReturn(List.of());
assertNotNull(service.reviewBlockReason("D",null,null),"缺失真实扫码事实不能推算补齐");
when(mapper.receivedPhysical(10,"C")).thenReturn(List.of());
when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(first,second));
assertNull(service.reviewBlockReason("D",null,null));
assertNull(service.departureBlockReason("D"));
verify(mapper,atLeastOnce()).pickedPhysical(30,"C");
}
@Test void repeatedPhysicalRowsForSameOrderResolveByIdentityWithoutIndexingDistinctResults() {
var sources=mock(AppointmentSourceService.class);
var one=new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:A",4,4);