完善预约货物资料与可选确认时间并修复排车衔接

This commit is contained in:
2026-10-28 11:57:09 +08:00
parent 96ffc0925a
commit 4f2d90b490
23 changed files with 233 additions and 56 deletions
@@ -9,17 +9,17 @@ import static org.mockito.Mockito.*;
class AppointmentBatchPreviewServiceTest {
private static AppointmentCargoVerificationService verification() {
var service=mock(AppointmentCargoVerificationService.class);
when(service.inspect(any())).thenAnswer(call->{AppointmentSourceService.Source source=call.getArgument(0);return new AppointmentCargoVerificationService.Context(source,null,0,0,source.packages(),source.packages()==null?null:source.packages().longValue(),false,List.of("VERIFY_CARGO"),List.of());});
when(service.inspect(any())).thenAnswer(call->{AppointmentSourceService.Source source=call.getArgument(0);return new AppointmentCargoVerificationService.Context(source,null,0,0,source.packages(),List.of(),source.packages()==null?null:source.packages().longValue(),false,List.of("VERIFY_CARGO"),List.of());});
return service;
}
@Test void previewUsesVerifiedRemainingQuantityAndDoesNotRejectAnEntirePartiallyAllocatedSource() {
var sources=mock(AppointmentSourceService.class);var mapper=mock(AppointmentBatchPreviewMapper.class);var verification=verification();
var refs=List.of(new AppointmentCoverageService.SourceRef("A","ORDER:1","FBA:ONT8"));var source=source("ORDER:1","A",1L,"100","FBA:ONT8");
when(sources.resolveBatch(refs)).thenReturn(List.of(source));when(mapper.flags(List.of(1L))).thenReturn(List.of(new AppointmentBatchPreviewMapper.Flags(1L,false,false,true)));
when(verification.inspect(source)).thenReturn(new AppointmentCargoVerificationService.Context(source,null,6,0,10,4L,false,List.of("VERIFY_CARGO"),List.of()));
when(verification.inspect(source)).thenReturn(new AppointmentCargoVerificationService.Context(source,null,6,0,10,List.of(),4L,false,List.of("VERIFY_CARGO"),List.of()));
var service=new AppointmentBatchPreviewService(sources,mapper,verification);
assertEquals(4,service.preview(refs).group().packages());
when(verification.inspect(source)).thenReturn(new AppointmentCargoVerificationService.Context(source,null,10,0,10,0L,false,List.of("VERIFY_CARGO"),List.of()));
when(verification.inspect(source)).thenReturn(new AppointmentCargoVerificationService.Context(source,null,10,0,10,List.of(),0L,false,List.of("VERIFY_CARGO"),List.of()));
assertThrows(ResponseStatusException.class,()->service.preview(refs));
}
private static AppointmentSourceService.Source source(String key,String customer,Long orderId,String origin,String destination) {
@@ -13,7 +13,7 @@ import static org.mockito.Mockito.*;
class AppointmentCargoVerificationBatchTest {
@Test void inspectionUsesTwoBoundedBatchesAndRetainsPerSourcePrerequisitesAndAllocations() {
var sources=mock(AppointmentSourceService.class);var mapper=mock(AppointmentCargoVerificationMapper.class);
var service=new AppointmentCargoVerificationService(sources,mapper,new ObjectMapper());
var service=new AppointmentCargoVerificationService(sources,mapper,new ObjectMapper(),new AppointmentCargoReferenceRepository(mapper));
var refs=IntStream.rangeClosed(1,200).mapToObj(i->new AppointmentCoverageService.SourceRef("C","ORDER:"+i,"FBA:ONT8")).toList();
when(sources.inspectBatch(anyList())).thenAnswer(call->call.<List<AppointmentCoverageService.SourceRef>>getArgument(0).stream().map(ref->
ref.sourceKey().equals("ORDER:2")?new AppointmentSourceService.Resolution(ref,null,new ResponseStatusException(HttpStatus.BAD_REQUEST,"接仓点不在启用的美国仓目录中")):
@@ -25,7 +25,7 @@ class AppointmentCargoVerificationBatchTest {
assertEquals("接仓点不在启用的美国仓目录中",assertThrows(ResponseStatusException.class,()->result.get(1).requireContext()).getReason());
assertEquals(10L,result.get(199).requireContext().availablePackages());
verify(sources,times(2)).inspectBatch(argThat(batch->batch.size()==100));
verify(mapper,times(2)).latestBatch(anyList());verify(mapper,times(2)).allocatedBatch(anyList());verifyNoMoreInteractions(mapper,sources);
verify(mapper,times(2)).latestBatch(anyList());verify(mapper,times(2)).allocatedBatch(anyList());verify(mapper,times(2)).originalShipmentReferences(anyList());verifyNoMoreInteractions(mapper,sources);
when(sources.inspectBatch(anyList())).thenThrow(new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,"目录不可用"));
assertEquals(503,assertThrows(ResponseStatusException.class,()->service.inspectBatch(refs)).getStatusCode().value());
}
@@ -24,6 +24,21 @@ class AppointmentCoverageServiceTest {
assertEquals("America/Los_Angeles",result.appointments().get(0).timezone()); assertEquals("EXT-1",result.appointments().get(0).confirmationNo());assertFalse(result.missingFields().contains("APPOINTMENT_TIMEZONE_UNVERIFIED"));
assertFalse(result.changeReviewRequired()); verifyNoInteractions(gateway);
}
@Test void confirmedNumberWithoutATimeRemainsConfirmedAndQuantityOnlyVerificationKeepsOriginalReferences() {
var mapper=mock(AppointmentCoverageMapper.class);
when(mapper.sources(anyList())).thenReturn(List.of(order()));
when(mapper.appointments(anyList())).thenReturn(List.of(new AppointmentCoverageMapper.Fact(1L,"A","AP-1","SUCCESS",null,null,AFTER,10L,20,"ONT8","ORDER:1","ORDER:2","EXT-1","America/Los_Angeles",1)));
var evidence=new AppointmentCargoVerification();evidence.setCustomerId("A");evidence.setSourceKey("ORDER:1");evidence.setDestinationKey("FBA:ONT8");
evidence.setSourceVersion("ORDER:2");evidence.setSourcePackages(20);evidence.setPackageCount(20);evidence.setVersion(1);evidence.setShipmentReferencesJson("[]");
when(mapper.verifications(anyList())).thenReturn(List.of(evidence));
var service=new AppointmentCoverageService(mapper,mock(AppointmentExternalSourceGateway.class));
var refs=List.of(new AppointmentCoverageService.SourceRef("A","ORDER:1","FBA:ONT8"));
var result=service.query(refs).get(0);
assertEquals("CONFIRMED",result.status());assertEquals("EXT-1",result.appointments().get(0).confirmationNo());
assertNull(result.appointments().get(0).confirmedAt());assertFalse(result.missingFields().contains("FBA_SHIPMENT_REFERENCES"));
when(mapper.sources(anyList())).thenReturn(List.of(new AppointmentCoverageMapper.Source(1L,"A","FBA","ONT8",20,2,BEFORE,true)));
assertTrue(service.query(refs).get(0).missingFields().contains("FBA_SHIPMENT_REFERENCES"));
}
@Test void partialPendingAndDuplicateClaimsNeverAppearAsCompleteCoverage() {
var mapper=mock(AppointmentCoverageMapper.class); when(mapper.sources(anyList())).thenReturn(List.of(order()));
var service=new AppointmentCoverageService(mapper,mock(AppointmentExternalSourceGateway.class));
@@ -26,6 +26,54 @@ class AppointmentCargoVerificationPersistenceTest {
private static void flags(AnnotationConfigApplicationContext context) {
when(context.getBean(AppointmentBatchPreviewMapper.class).flags(List.of(1L))).thenReturn(List.of(new AppointmentBatchPreviewMapper.Flags(1L,false,false,false)));
}
@Test void readsOriginalShipmentAndTrackingNumbersForTheAuthorizedCustomerAndKeepsExplicitVerificationFirst() {
try(var context=new AnnotationConfigApplicationContext(FbaAppointmentPersistenceTest.Config.class)) {
var db=new JdbcTemplate(context.getBean(DataSource.class));
db.update("UPDATE ll_order SET customer_id='A' WHERE id=1");
db.update("INSERT INTO ll_order_detail(id,order_id,customer_id,shipment_no,fba_tracking_no) VALUES(1,1,'A',' FBA-ORIGINAL ','TRACK-ORIGINAL'),(2,1,'B','OTHER-CUSTOMER','OTHER-TRACK')");
var cargo=context.getBean(AppointmentCargoVerificationService.class);
var source=context.getBean(AppointmentSourceService.class).resolveBatch(List.of(new AppointmentCoverageService.SourceRef("A","ORDER:1","FBA:ONT8"))).get(0);
var first=cargo.inspect(source);
assertNull(first.verification());assertEquals(List.of("FBA-ORIGINAL","TRACK-ORIGINAL"),first.effectiveShipmentReferences());
cargo.save("ORDER:1","A",save(0,10,"quantity-only"),"operator","客服");
assertEquals(first.effectiveShipmentReferences(),cargo.inspect(source).effectiveShipmentReferences());
var details=new FbaAppointmentDetails(java.time.LocalDate.of(2026,10,28),null,null,"EXT-ORIGINAL",null,null,"SUCCESS",null,null,null);
var appointment=context.getBean(InboundFbaAppointmentService.class).createFromSources("operator",appointment("reuse-original",details));
assertEquals(first.effectiveShipmentReferences(),appointment.items().get(0).shipmentReferences());
assertNull(appointment.appointmentAt());assertEquals("SUCCESS",appointment.appointmentStatus());
cargo.save("ORDER:1","A",save(1,10,"explicit","FBA-VERIFIED"),"operator","客服");
assertEquals(List.of("FBA-VERIFIED"),cargo.inspect(source).effectiveShipmentReferences());
assertEquals(" FBA-ORIGINAL ",db.queryForObject("SELECT shipment_no FROM ll_order_detail WHERE id=1",String.class));
db.update("UPDATE ll_order SET order_type='WAREHOUSE_TRANSFER' WHERE id=1");
db.update("INSERT INTO ll_warehouse_transfer_order(id,lastleg_order_id,customer_id,fba_shipment_no,tracking_no) VALUES(1,1,'A','TRANSFER-FBA','TRANSFER-TRACK')");
assertEquals(List.of("TRANSFER-FBA","TRANSFER-TRACK"),context.getBean(AppointmentCargoReferenceRepository.class).originalReferences(List.of(source)).get(List.of("A","ORDER:1")));
}
}
/** 采用最新资料与新建共用货件号优先级:未核实、仅核箱数和明确核实均写入真实快照。 */
@Test void adoptingLatestCargoKeepsOriginalReferencesUntilExplicitVerificationReplacesThem() {
try(var context=new AnnotationConfigApplicationContext(FbaAppointmentPersistenceTest.Config.class)) {
var db=new JdbcTemplate(context.getBean(DataSource.class));
db.update("UPDATE ll_order SET customer_id='A' WHERE id=1");
db.update("INSERT INTO ll_order_detail(id,order_id,customer_id,shipment_no,fba_tracking_no) VALUES(1,1,'A','FBA-ORIGINAL','TRACK-ORIGINAL'),(2,1,'B','OTHER-CUSTOMER','OTHER-TRACK')");
var cargo=context.getBean(AppointmentCargoVerificationService.class);
var appointments=context.getBean(InboundFbaAppointmentService.class);
var created=appointments.createFromSources("operator",appointment("refresh-original",confirmed()));
var transaction=new org.springframework.transaction.support.TransactionTemplate(context.getBean(org.springframework.jdbc.datasource.DataSourceTransactionManager.class));
var original=List.of("FBA-ORIGINAL","TRACK-ORIGINAL");
assertEquals(original,created.items().get(0).shipmentReferences());
db.update("UPDATE ll_order_detail SET shipment_no='FBA-LATEST' WHERE id=1");
var latestOriginal=List.of("FBA-LATEST","TRACK-ORIGINAL");
for(int version=0;version<=2;version++) {
if(version==1)cargo.save("ORDER:1","A",save(0,10,"refresh-quantity-only"),"operator","客服");
if(version==2)cargo.save("ORDER:1","A",save(1,10,"refresh-explicit","FBA-VERIFIED"),"operator","客服");
Integer expectedVersion=version==0?null:version;
var latest=transaction.execute(status->appointments.refreshReceivingCargo(created.batchKey(),"A",List.of(new com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.CargoVersion("ORDER:1","FBA:ONT8","ORDER:1",expectedVersion))));
assertNotNull(latest);assertEquals(version==2?List.of("FBA-VERIFIED"):latestOriginal,latest.items().get(0).shipmentReferences());
assertEquals(latest,appointments.detail(created.appointmentNo()),"刷新后的货件号必须实际持久化");
}
assertEquals("FBA-LATEST",db.queryForObject("SELECT shipment_no FROM ll_order_detail WHERE id=1",String.class));
}
}
@Test void savesWithoutAppointmentOrConfirmationAndReplaysOriginalVersionAfterLaterEdits() {
try(var context=new AnnotationConfigApplicationContext(FbaAppointmentPersistenceTest.Config.class)) {
var service=context.getBean(AppointmentCargoVerificationService.class);
@@ -211,7 +211,9 @@ class FbaAppointmentPersistenceTest {
int start=baseline.indexOf("CREATE TABLE IF NOT EXISTS "+table+" (");assertTrue(start>=0);
String sql=baseline.substring(start,baseline.indexOf(';',start)+1).replace(" STORED","").replaceAll(" ENGINE=InnoDB[^;]*;",";");statement.execute(sql);
}
statement.execute("CREATE TABLE ll_order(id BIGINT PRIMARY KEY,version INT DEFAULT 0,customer_id VARCHAR(128),customer_name VARCHAR(128))");statement.execute("INSERT INTO ll_order(id) VALUES(1),(2)");
statement.execute("CREATE TABLE ll_order(id BIGINT PRIMARY KEY,version INT DEFAULT 0,customer_id VARCHAR(128),customer_name VARCHAR(128),order_type VARCHAR(32) DEFAULT 'FBA')");statement.execute("INSERT INTO ll_order(id) VALUES(1),(2)");
statement.execute("CREATE TABLE ll_order_detail(id BIGINT PRIMARY KEY,order_id BIGINT,customer_id VARCHAR(128),shipment_no VARCHAR(128),fba_tracking_no VARCHAR(128))");
statement.execute("CREATE TABLE ll_warehouse_transfer_order(id BIGINT PRIMARY KEY,lastleg_order_id BIGINT,customer_id VARCHAR(128),fba_shipment_no VARCHAR(128),tracking_no VARCHAR(128))");
statement.execute("CREATE TABLE ll_dispatch_item(id BIGINT,order_id BIGINT,customer_id VARCHAR(128),dispatch_order_id BIGINT,plan_pallet_id BIGINT,source_stage VARCHAR(32),package_count INT)");
statement.execute("CREATE TABLE ll_dispatch_order(id BIGINT,dispatch_no VARCHAR(64),record_version BIGINT,status VARCHAR(32))");
String work;try(var input=FbaAppointmentPersistenceTest.class.getResourceAsStream("/db/appointment-work.sql")){work=new String(Objects.requireNonNull(input).readAllBytes(),StandardCharsets.UTF_8);}
@@ -254,7 +256,8 @@ class FbaAppointmentPersistenceTest {
}
@Bean FbaReceivingService receivingService(FbaReceivingStore store,InboundFbaAppointmentService batches,FbaAppointmentTransportPort transport,FbaAppointmentCommandMapper commands,FbaAppointmentWorkPort work,FbaAppointmentExecutionPort execution){return new FbaReceivingService(store,batches,transport,commands,work,execution);}
@Bean AppointmentCargoVerificationMapper verifications(SqlSessionTemplate session){return session.getMapper(AppointmentCargoVerificationMapper.class);}
@Bean AppointmentCargoVerificationService verificationService(AppointmentSourceService sources,AppointmentCargoVerificationMapper mapper,ObjectMapper json){return new AppointmentCargoVerificationService(sources,mapper,json);}
@Bean AppointmentCargoReferenceRepository cargoReferences(AppointmentCargoVerificationMapper mapper){return new AppointmentCargoReferenceRepository(mapper);}
@Bean AppointmentCargoVerificationService verificationService(AppointmentSourceService sources,AppointmentCargoVerificationMapper mapper,ObjectMapper json,AppointmentCargoReferenceRepository references){return new AppointmentCargoVerificationService(sources,mapper,json,references);}
@Bean AppointmentSourceService sources(){var mock=mock(AppointmentSourceService.class);when(mock.resolveBatch(anyList())).thenAnswer(call->{List<AppointmentCoverageService.SourceRef> refs=call.getArgument(0);return refs.stream().map(ref->cargo(20).stream().filter(s->s.sourceKey().equals(ref.sourceKey()) && s.customerId().equals(ref.customerId())).findFirst().orElseThrow()).toList();});when(mock.destinationTimezone("ONT8")).thenReturn("America/Los_Angeles");return mock;}
@Bean AppointmentBatchPreviewMapper flags(){var mock=mock(AppointmentBatchPreviewMapper.class);when(mock.flags(anyList())).thenAnswer(call->{List<Long> ids=call.getArgument(0);return ids.stream().map(id->new AppointmentBatchPreviewMapper.Flags(id,false,false,false)).toList();});return mock;}
@Bean ObjectMapper json(){return new ObjectMapper().findAndRegisterModules();}
@@ -138,6 +138,16 @@ class InboundFbaAppointmentServiceTest {
assertThrows(ResponseStatusException.class,()->service.createFromSources("other",request("original",pending(null),"ORDER:1","ORDER:2")));
verify(headers,times(1)).insert(any(InboundFbaAppointment.class));
}
@Test void savesExternalConfirmationWithoutInventingAConfirmedTimeFromThePlannedDate() {
var details=new FbaAppointmentDetails(LocalDate.of(2026,10,28),null,null,"EXT-NO-TIME",null,null,"SUCCESS",null,null,null);
var result=service.createFromSources("operator",request("confirmed-without-time",details,"ORDER:1"));
assertEquals("SUCCESS",result.appointmentStatus());assertEquals("EXT-NO-TIME",result.confirmationNo());
assertEquals(LocalDate.of(2026,10,28),result.plannedDate());assertNull(result.appointmentAt());
assertNull(result.appointmentEndAt());assertNull(result.appointmentStartUtc());assertNull(result.appointmentEndUtc());
assertEquals(service.detail(result.appointmentNo()),result);
var invalid=new FbaAppointmentDetails(null,null,null,"EXT-END-ONLY",null,at,"SUCCESS",null,null,null);
assertThrows(ResponseStatusException.class,()->service.createFromSources("operator",request("end-only",invalid,"ORDER:2")));
}
@Test void confirmationNeedsExternalNumberAndLocationTimezone() {
var missing=new FbaAppointmentDetails(null,null,null,null,at,null,"SUCCESS",null,null,null);
assertThrows(ResponseStatusException.class,()->service.createFromSources("operator",request("no-number",missing,"ORDER:1")));
@@ -12,17 +12,29 @@ import static org.mockito.Mockito.*;
/** 选入排车前的确认资格按真实客户和逐票覆盖核对。 */
class AppointmentDispatchAvailabilityTest {
private final AppointmentDispatchAvailability policy=new AppointmentDispatchAvailability(mock(FbaReceivingStore.class));
/** 预约头成功不能替代当前客户的有效确认,取消及无时间均不放行。 */
/** 正式确认可以尚无时段,仍须匹配当前客户、来源和目的仓;取消和未确认均不放行。 */
@Test void onlyCurrentConfirmedCustomerSourceAndDestinationCanEnterDispatch() {
assertThat(count(snapshot("ACTIVE","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isEqualTo(20);
assertThat(count(snapshot("ACTIVE","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"B"),"A")).isZero();
assertThat(count(snapshot("CANCELLED","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isZero();
assertThat(count(snapshot("ACTIVE","PENDING","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isZero();
assertThat(count(snapshot("ACTIVE","SUCCESS","America/New_York",null,"A"),"A")).isZero();
assertThat(count(snapshot("ACTIVE","SUCCESS","America/New_York",null,"A"),"A")).isEqualTo(20);
var confirmed=snapshot("ACTIVE","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A");
assertThat(policy.confirmedPackages(confirmed,"A","ORDER:2","FBA:ACY2")).isZero();
assertThat(policy.confirmedPackages(confirmed,"A","ORDER:1","FBA:ONT8")).isZero();
}
/** 没有确认时间不代表可以省略外部约号,或单独填写结束时间及偏移。 */
@Test void untimedConfirmationStillRequiresExternalNumberAndConsistentOptionalWindow() {
var before=snapshot("ACTIVE","SUCCESS","America/New_York",null,"A");var old=before.agreements().get(0);
for(var details:List.of(
new FbaAppointmentDetails(null,null,null," ",null,null,"SUCCESS",null,null,null),
new FbaAppointmentDetails(null,null,null,"CONF-1",null,LocalDateTime.of(2026,10,26,10,0),"SUCCESS",null,null,null),
new FbaAppointmentDetails(null,null,null,"CONF-1",null,null,"SUCCESS",null,"-04:00",null),
new FbaAppointmentDetails(null,null,null,"CONF-1",null,null,"SUCCESS",null,null,"-04:00"))) {
var agreement=new Agreement(old.agreementKey(),old.version(),old.status(),old.destinationKey(),details,old.timezone(),old.members(),old.warehouseEvidence());
assertThat(count(new BatchReceiving(before.batchKey(),List.of(agreement),List.of(),List.of()),"A")).isZero();
}
}
/** 夏令时歧义及未维护时区不能猜成一个可排时间。 */
@Test void invalidOrAmbiguousLocalTimeCannotAuthorizeDispatch() {
assertThat(count(snapshot("ACTIVE","SUCCESS",null,LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isZero();
@@ -192,7 +192,7 @@ class AppointmentWorkDeliveryProviderTest {
@Test void handledPrerequisiteCommitsTheCallerTransactionThroughTheRealCargoProxy() {
var sourceService=mock(com.baiyue.lastleg.appointmentplanning.AppointmentSourceService.class);
when(sourceService.inspectBatch(List.of(new SourceRef("C","ORDER:11",null)))).thenReturn(List.of(new Resolution(new SourceRef("C","ORDER:11",null),null,new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"当前货源类型不支持FBA预约"))));
var cargo=new AppointmentCargoVerificationService(sourceService,mock(com.baiyue.lastleg.appointmentplanning.AppointmentCargoVerificationMapper.class),new com.fasterxml.jackson.databind.ObjectMapper());
var cargo=new AppointmentCargoVerificationService(sourceService,mock(com.baiyue.lastleg.appointmentplanning.AppointmentCargoVerificationMapper.class),new com.fasterxml.jackson.databind.ObjectMapper(),mock(com.baiyue.lastleg.appointmentplanning.AppointmentCargoReferenceRepository.class));
var ds=new org.h2.jdbcx.JdbcDataSource();ds.setURL("jdbc:h2:mem:work-read-tx-"+java.util.UUID.randomUUID()+";DB_CLOSE_DELAY=-1");
var manager=new org.springframework.jdbc.datasource.DataSourceTransactionManager(ds);
var proxy=new org.springframework.aop.framework.ProxyFactory(cargo);proxy.setProxyTargetClass(true);
@@ -210,6 +210,26 @@ class AppointmentWorkDispatchPersistenceTest {
assertEquals(1,rows.size());assertEquals("DISPATCH:1",rows.get(0).get("BATCH_KEY"));
assertEquals("101",rows.get(0).get("CUSTOMER_ID"));assertEquals(physical.packages(),rows.get(0).get("PACKAGE_COUNT"));
}
/** 没有确认时段的正式预约仍能带入本车,读写分配均保留真实数量且不虚构时间。 */
@Test void confirmedAppointmentWithoutTimeCanBeSelectedAndLinkedToDispatch() {
var original=confirmed("101");
var details=new FbaAppointmentDetails(null,null,null,original.details().confirmationNo(),null,null,"SUCCESS",null,null,null);
var untimed=new com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.Agreement(original.agreementKey(),original.version(),original.status(),original.destinationKey(),details,original.timezone(),original.members(),original.warehouseEvidence());
when(receiving.currentAgreement(batch,"101")).thenReturn(untimed);
when(receiving.snapshots(anyList())).thenReturn(List.of(new com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.BatchReceiving(batch,List.of(untimed,confirmed("202")),List.of(),List.of())));
var identity=new CandidatePhysical("101",111L);
var reference=candidates.batchReferences(List.of(identity)).get(identity);
assertTrue(reference.blockReasons().isEmpty());assertEquals(80,reference.appointmentBatches().get(0).sources().get(0).packageCount());
var candidate=candidates.read(batch).candidates().stream().filter(c->"101".equals(c.customerId())).findFirst().orElseThrow();
assertTrue(candidate.blockReasons().isEmpty());assertEquals(80,candidate.packageCount());
var source=binding.allocationOptions(1,List.of(identity)).get(identity).get(0);
assertTrue(source.blockReasons().isEmpty());assertEquals(1,source.options().size());
var option=source.options().get(0);assertEquals("CONF-101",option.confirmationNo());assertEquals(80,option.availablePackages());
assertNull(option.appointmentAt());assertNull(option.appointmentEndAt());
tx.executeWithoutResult(s->{bind(11,21,22);binding.bindItems(1,List.of(item(31)),List.of());});
assertEquals(80,jdbc.queryForObject("SELECT package_count FROM ll_dispatch_appointment_allocation WHERE dispatch_item_id=11",Integer.class));
assertNull(tx.execute(status->binding.reviewBlockReason("D-1",null,null)));
}
/** 查询本车重分配量排除本车原占用,保留其他车占用及真实业务标识。 */
@Test void ownedAllocationOptionsExcludeOnlyThisDispatchAndKeepOtherCarsQuota() {
tx.executeWithoutResult(s->bind(11));