fix(appointment): exclude cancelled batches without active allocations

This commit is contained in:
2026-10-16 16:21:51 +08:00
parent 000125c3ec
commit 58685945b1
3 changed files with 62 additions and 8 deletions
@@ -60,14 +60,19 @@ class AppointmentWorkSyncServiceTest {
when(f.gateway.discover(null,10)).thenReturn(new SourcePage(List.of(container),null,true,"scope"));
assertThrows(ResponseStatusException.class,()->f.sync.synchronize(new SyncRequest(null,10)));
}
/** 空取消批次回到原货源时退役客户送仓及运输事项,并使版本门禁恢复可读。 */
@Test void cancellationReopensOriginalDemandAndRestoringTheSameAppointmentDoesNotDuplicateWork() {
var f=new WorkTestFixture();
var root=new Requirement("INBOUND:1",null,null,"FBA",null,"ONT8",null,List.of(),List.of("FBA_APPOINTMENT:1"),List.of());
var batch=new Requirement("FBA_APPOINTMENT:1","FBA_APPOINTMENT:1","AP-1","FBA",null,"ONT8",null,List.of(),List.of("INBOUND:1"),List.of(new SourceReference("C","ORDER:1","FBA:ONT8")));
var refs=List.of(new SourceReference("C","ORDER:1","FBA:ONT8"));
var root=new Requirement("INBOUND:1",null,null,"FBA",null,"ONT8",null,List.of(),List.of("FBA_APPOINTMENT:1","FBA_APPOINTMENT:1|CUSTOMER:C"),refs);
var batch=new Requirement("FBA_APPOINTMENT:1|CUSTOMER:C","FBA_APPOINTMENT:1","AP-1","FBA",null,"ONT8",null,List.of(),List.of("INBOUND:1"),refs);
for(var requirement:List.of(root,batch,root,batch)) {
f.guard.setRequirementVersion(f.guard.getRequirementVersion()+1);
assertThrows(ResponseStatusException.class,f.store::requireInitialized);
var source=new ContainerSource("1","CNT",null,LocalDateTime.parse("2026-10-01T10:00:00"),null,"v1",Flow.TRANSFER,null,List.of(requirement));
when(f.gateway.discover(null,10)).thenReturn(new SourcePage(List.of(source),null,true,"scope"));
f.sync.synchronize(new SyncRequest(null,10));
assertDoesNotThrow(f.store::requireInitialized);
var deliveries=f.store.all().stream().filter(s->s.getType()==Type.DELIVERY && !s.isRetired()).toList();
assertEquals(1,deliveries.size()); assertEquals(requirement.requirementKey(),deliveries.get(0).getRequirementKey());
}
@@ -11,6 +11,47 @@ import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*;
import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*;
class AppointmentWorkRequirementProviderTest {
/** 取消历史没有活动货物时退役旧事项,仍按权威分配保留新批次和未分配余量。 */
@Test void cancelledEmptyHistoryDoesNotBlockCurrentBatchOrRestoreAlreadyAllocatedCargo() {
var sources=mock(AppointmentSourceService.class);var cargo=mock(AppointmentCargoVerificationService.class);
var appointments=mock(InboundFbaAppointmentService.class);var mapping=mock(AppointmentWorkSourceMapper.class);
var provider=new AppointmentWorkRequirementProvider(sources,cargo,appointments,mapping);
var requirement=new Requirement("INBOUND:1",null,null,"FBA",null,"ONT8",null,List.of(),List.of(),List.of());
var container=new ContainerSource("1","CNT",null,null,LocalDateTime.parse("2026-10-15T11:00:00"),"1",Flow.TRANSFER,null,List.of(requirement));
when(mapping.containerSources("1")).thenReturn(List.of(new AppointmentWorkSourceMapper.Source(11L,"C","10","1","FBA","ONT8",60)));
when(sources.canonicalOrders(anyList())).thenReturn(List.of(new AppointmentCoverageService.SourceRef("C","ORDER:11",null)));
var source=new AppointmentSourceService.Source("ORDER:11","C",11L,"IN-11",null,"CNT",null,null,"FBA:ONT8","ONT8",60,"v1",true,List.of());
when(sources.resolveBatch(anyList())).thenReturn(List.of(source));
var allocation=mock(AppointmentCargoVerificationService.Context.class);
when(cargo.inspect(source)).thenReturn(allocation);when(allocation.availablePackages()).thenReturn(0L);
var cancelled=mock(com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse.class);
when(cancelled.batchKey()).thenReturn("FBA_APPOINTMENT:1");when(cancelled.appointmentStatus()).thenReturn("CANCELLED");
when(cancelled.items()).thenReturn(List.of());when(cancelled.replacementBatchKeys()).thenReturn(List.of());
var current=mock(com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse.class);
var item=mock(com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse.Item.class);
when(item.customerId()).thenReturn("C");when(item.sourceKey()).thenReturn("ORDER:11");when(item.destinationKey()).thenReturn("FBA:ONT8");
when(current.batchKey()).thenReturn("FBA_APPOINTMENT:2");when(current.appointmentNo()).thenReturn("AP-2");
when(current.fbaWarehouseCode()).thenReturn("ONT8");when(current.items()).thenReturn(List.of(item));when(current.replacementBatchKeys()).thenReturn(List.of());
when(appointments.relatedBatchSnapshots(anyList())).thenReturn(List.of(cancelled,current));
var actual=provider.requirements(container,Flow.TRANSFER);
assertEquals(List.of("FBA_APPOINTMENT:2|CUSTOMER:C"),actual.stream().map(Requirement::requirementKey).toList());
assertEquals(List.of(new SourceReference("C","ORDER:11","FBA:ONT8")),actual.get(0).sources());
assertTrue(actual.get(0).replacesRequirementKeys().contains("FBA_APPOINTMENT:1|CUSTOMER:C"));
when(allocation.availablePackages()).thenReturn(20L);
actual=provider.requirements(container,Flow.TRANSFER);
assertEquals(List.of("INBOUND:1","FBA_APPOINTMENT:2|CUSTOMER:C"),actual.stream().map(Requirement::requirementKey).toList());
assertEquals(List.of("FBA_APPOINTMENT:1","FBA_APPOINTMENT:1|CUSTOMER:C"),actual.get(0).replacesRequirementKeys());
when(appointments.relatedBatchSnapshots(anyList())).thenReturn(List.of(cancelled));
when(allocation.availablePackages()).thenReturn(60L);
actual=provider.requirements(container,Flow.TRANSFER);
assertEquals(List.of("INBOUND:1"),actual.stream().map(Requirement::requirementKey).toList());
assertEquals(List.of(new SourceReference("C","ORDER:11","FBA:ONT8")),actual.get(0).sources());
assertTrue(actual.get(0).replacesRequirementKeys().contains("FBA_APPOINTMENT:1|CUSTOMER:C"));
// 非取消的空批次仍属缺失事实,不能静默跳过。
when(cancelled.appointmentStatus()).thenReturn("PENDING");
assertEquals(503,assertThrows(org.springframework.web.server.ResponseStatusException.class,()->provider.requirements(container,Flow.TRANSFER)).getStatusCode().value());
}
@Test void cancelledAndFailedBookingsRetainTheirAllocatedBatchAndOnlyAuthorityDefinesRemainder() {
var sources=mock(AppointmentSourceService.class);var cargo=mock(AppointmentCargoVerificationService.class);
var appointments=mock(InboundFbaAppointmentService.class);var mapping=mock(AppointmentWorkSourceMapper.class);