diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProvider.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProvider.java index 8d37dbb0e..1bae1c9f4 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProvider.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProvider.java @@ -84,6 +84,8 @@ public class AppointmentWorkRequirementProvider implements AppointmentWorkFactGa customers.forEach((customer,refs)->{ var replacements=new LinkedHashSet<>(requirement.replacesRequirementKeys()); requirement.replacesRequirementKeys().stream().filter(k->k.startsWith("FBA_APPOINTMENT:")).map(k->customerRequirement(k,customer)).forEach(replacements::add); + // 客户事项承接同一实物批次的整体需求;批次身份未变化也须有明确拆分去向。 + replacements.add(requirement.batchKey()); scoped.add(new Requirement(customerRequirement(requirement.batchKey(),customer),requirement.batchKey(),requirement.batchNo(),requirement.destinationKind(), requirement.originName(),requirement.destinationName(),requirement.timezone(),requirement.references(),List.copyOf(replacements),List.copyOf(refs))); }); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSyncServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSyncServiceTest.java index 6a17d68cf..b44348ca7 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSyncServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSyncServiceTest.java @@ -12,6 +12,45 @@ import static com.baiyue.lastleg.appointmentwork.AppointmentWorkModels.*; import static com.baiyue.lastleg.appointmentwork.AppointmentWorkFactGateway.*; class AppointmentWorkSyncServiceTest { + /** 同一实物批次拆成客户事项时,整体事项须有完整去向,运输仍保持唯一。 */ + @Test void batchWideDeliveryIsReplacedByItsExplicitCustomerRequirements() { + var f=new WorkTestFixture();String batch="FBA_APPOINTMENT:1"; + var refs=List.of(new SourceReference("A","ORDER:1","FBA:ONT8"),new SourceReference("B","ORDER:2","FBA:ONT8")); + // 先保存整体需求的真实历史事项,再按相同批次和来源拆出客户事项。 + var old=f.state("1",Type.DELIVERY,Flow.TRANSFER,batch); + old.setWorkItemKey(AppointmentWorkPolicy.key("BATCH:"+batch,Type.DELIVERY,null)); + old.setBatchKey(batch);old.setBatchKeys(List.of(batch));old.setSources(refs);f.add(old); + var sources=mock(com.baiyue.lastleg.appointmentplanning.AppointmentSourceService.class); + var cargo=mock(com.baiyue.lastleg.appointmentplanning.AppointmentCargoVerificationService.class); + var appointments=mock(com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService.class); + var mapping=mock(com.baiyue.lastleg.inboundplan.work.AppointmentWorkSourceMapper.class); + var provider=new com.baiyue.lastleg.inboundplan.work.AppointmentWorkRequirementProvider(sources,cargo,appointments,mapping); + when(mapping.containerSources("1")).thenReturn(List.of(new com.baiyue.lastleg.inboundplan.work.AppointmentWorkSourceMapper.Source(1L,"A","1","1","FBA","ONT8",1),new com.baiyue.lastleg.inboundplan.work.AppointmentWorkSourceMapper.Source(2L,"B","2","1","FBA","ONT8",1))); + when(sources.canonicalOrders(anyList())).thenReturn(refs.stream().map(r->new com.baiyue.lastleg.appointmentplanning.AppointmentCoverageService.SourceRef(r.customerId(),r.sourceKey(),r.destinationKey())).toList()); + when(sources.resolveBatch(anyList())).thenReturn(refs.stream().map(r->new com.baiyue.lastleg.appointmentplanning.AppointmentSourceService.Source(r.sourceKey(),r.customerId(),Long.valueOf(r.sourceKey().substring(6)),null,null,"CNT",null,null,r.destinationKey(),"ONT8",1,"v1",true,List.of())).toList()); + var allocation=mock(com.baiyue.lastleg.appointmentplanning.AppointmentCargoVerificationService.Context.class); + when(allocation.availablePackages()).thenReturn(0L);when(cargo.inspect(any(com.baiyue.lastleg.appointmentplanning.AppointmentSourceService.Source.class))).thenReturn(allocation); + var snapshot=mock(com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse.class); + when(snapshot.batchKey()).thenReturn(batch);when(snapshot.appointmentNo()).thenReturn("AP-1");when(snapshot.fbaWarehouseCode()).thenReturn("ONT8"); + var items=refs.stream().map(r->{var item=mock(com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse.Item.class);when(item.customerId()).thenReturn(r.customerId());when(item.sourceKey()).thenReturn(r.sourceKey());when(item.destinationKey()).thenReturn(r.destinationKey());return item;}).toList(); + when(snapshot.items()).thenReturn(items); + when(appointments.relatedBatchSnapshots(anyList())).thenReturn(List.of(snapshot)); + var root=new Requirement("INBOUND:1",null,null,"FBA",null,"ONT8",null,List.of(),List.of(),refs); + var source=new ContainerSource("1","CNT",null,LocalDateTime.parse("2026-10-01T10:00:00"),null,"v1",Flow.TRANSFER,null,List.of(root)); + doAnswer(c->provider.requirements(c.getArgument(0),c.getArgument(1))).when(f.gateway).requirements(any(),eq(Flow.TRANSFER)); + when(f.gateway.discover(null,10)).thenReturn(new SourcePage(List.of(source),null,true,"scope")); + f.guard.setRequirementVersion(1); + f.sync.synchronize(new SyncRequest(null,10)); + var deliveries=f.store.all().stream().filter(s->s.getType()==Type.DELIVERY && !s.isRetired()).toList(); + assertEquals(2,deliveries.size()); + var retired=f.store.get(old.getWorkItemKey());assertTrue(retired.isRetired()); + assertEquals(new HashSet<>(deliveries.stream().map(AppointmentWorkState::getWorkItemKey).toList()),new HashSet<>(retired.getReplacementKeys())); + assertEquals(1,f.store.all().stream().filter(s->s.getType()==Type.TRANSPORT && !s.isRetired()).count()); + assertDoesNotThrow(f.store::requireInitialized); + f.sync.synchronize(new SyncRequest(null,10)); + assertEquals(2,f.store.all().stream().filter(s->s.getType()==Type.DELIVERY && !s.isRetired()).count()); + } + @Test void onePhysicalBatchHasIndependentCustomerDeliveriesAndOneVehicleWithAllCustomerEdges() { var f=new WorkTestFixture();String batch="FBA_APPOINTMENT:1"; var requirements=List.of("A","B").stream().map(customer->new Requirement(AppointmentWorkPolicy.customerRequirement(batch,customer),batch,"AP-1","FBA","海外仓","ONT8",null,List.of(),List.of(),List.of(new SourceReference(customer,"ORDER:"+customer,"FBA:ONT8")))).toList(); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProviderTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProviderTest.java index 7a4ccc676..1ff8f4d24 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProviderTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProviderTest.java @@ -75,6 +75,7 @@ class AppointmentWorkRequirementProviderTest { var result=provider.requirements(container,Flow.TRANSFER); assertEquals(List.of("FBA_APPOINTMENT:1|CUSTOMER:C"),result.stream().map(Requirement::requirementKey).toList()); assertEquals(List.of(new SourceReference("C","ORDER:11","FBA:ONT8")),result.get(0).sources()); + assertTrue(result.get(0).replacesRequirementKeys().contains("FBA_APPOINTMENT:1")); } when(allocation.availablePackages()).thenReturn(20L); assertEquals(List.of("INBOUND:1","FBA_APPOINTMENT:1|CUSTOMER:C"),provider.requirements(container,Flow.TRANSFER).stream().map(Requirement::requirementKey).toList());