This commit is contained in:
2026-10-22 14:37:59 +08:00
parent de96e47d74
commit 760bf8daa1
49 changed files with 2450 additions and 117 deletions
@@ -20,7 +20,7 @@ import static org.mockito.Mockito.*;
import static com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.*;
class AppointmentWorkPreArrangementPersistenceTest {
@Test void siblingPlanChangeCannotGrantOrRemovePhysicalPreArrangementAndNeverAuthorizesDeparture() {
@Test void siblingPlanChangeNeverAuthorizesUnconfirmedFbaCargoToEnterDispatch() {
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));
@@ -40,17 +40,17 @@ class AppointmentWorkPreArrangementPersistenceTest {
doReturn(List.of(new AppointmentWorkDispatchMapper.Physical(2L,"D","FBA:ONT8",40,40))).when(mapper).physical(102L,"D");
var jdbc=new JdbcTemplate(context.getBean(DataSource.class));jdbc.update("INSERT INTO ll_dispatch_order(id,dispatch_no,record_version,status) VALUES(20,'DP-B',0,'ACTIVE')");
jdbc.update("INSERT INTO ll_dispatch_item(id,customer_id,dispatch_order_id,plan_pallet_id,source_stage,package_count) VALUES(201,'B',20,101,'RECEIVED',60)");
var candidates=new AppointmentWorkDispatchCandidates(mapper,batches,sources);
var candidates=new AppointmentWorkDispatchCandidates(mapper,batches,sources,new AppointmentDispatchAvailability(store));
var binding=new AppointmentWorkDispatchBinding(mapper,batches,sources,mock(AppointmentWorkChanges.class),store,candidates);
var tx=new TransactionTemplate(context.getBean(PlatformTransactionManager.class));
var before=candidates.read(batch.batchKey()).candidates().stream().filter(c->c.customerId().equals("B")).findFirst().orElseThrow();
assertTrue(before.blockReasons().isEmpty());assertEquals(60,before.packageCount());
assertBindable(tx,binding,mapper,before);
assertFalse(before.blockReasons().isEmpty());assertEquals(60,before.packageCount());
assertNotBindable(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());assertBindable(tx,binding,mapper,after);
assertNotEquals(before.allocations().get(0).recordVersion(),after.allocations().get(0).recordVersion());assertNotBindable(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("尚无有效收货确认"));
@@ -65,13 +65,11 @@ class AppointmentWorkPreArrangementPersistenceTest {
assertTrue(mapper.byDispatch(20).isEmpty());
}
}
private static void assertBindable(TransactionTemplate tx,AppointmentWorkDispatchBinding binding,AppointmentWorkDispatchMapper mapper,AppointmentWorkDispatchCandidates.Candidate candidate) {
tx.executeWithoutResult(status->{
private static void assertNotBindable(TransactionTemplate tx,AppointmentWorkDispatchBinding binding,AppointmentWorkDispatchMapper mapper,AppointmentWorkDispatchCandidates.Candidate candidate) {
assertThrows(ResponseStatusException.class,()->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());
assertEquals(60,mapper.byDispatch(20).stream().mapToInt(AppointmentWorkDispatchMapper.Bound::packages).sum());
status.setRollbackOnly(); // Compare identical physical availability before and after the independent customer's plan command.
});
}));
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);}
@@ -51,6 +51,7 @@ class AppointmentWorkDispatchPersistenceTest {
private AppointmentWorkDispatchCandidates candidates;
private InboundFbaAppointmentService appointments;
private AppointmentSourceService sources;
private FbaReceivingStore receiving;
private final String batch="FBA_APPOINTMENT:1";
/** 无客户主单,多个客户可在同车同批;真实计划逐票数量来自各自明细。 */
@@ -78,8 +79,21 @@ class AppointmentWorkDispatchPersistenceTest {
when(appointments.batchContext(batch)).thenAnswer(c->new FbaAppointmentBatchContext(snapshot(),List.of(),List.of(),List.of(),List.of(),List.of()));
when(sources.canonicalOrders(anyList())).thenAnswer(c->{List<AppointmentExternalSourceGateway.OrderRef> orders=c.getArgument(0);return orders.stream().map(o->new AppointmentCoverageService.SourceRef(o.customerId(),o.orderId()==2?"BOOKING:real-2":"ORDER:"+o.orderId(),null)).toList();});
doAnswer(c->{jdbc.update("UPDATE change_guard SET version=version+1 WHERE id=1");return null;}).when(changes).requirementsChanged();
candidates=new AppointmentWorkDispatchCandidates(mapper,appointments,sources);
var proxy=new ProxyFactory(new AppointmentWorkDispatchBinding(mapper,appointments,sources,changes,mock(FbaReceivingStore.class),candidates));proxy.setProxyTargetClass(true);proxy.addAdvice(new TransactionInterceptor(manager,new AnnotationTransactionAttributeSource()));binding=(AppointmentWorkDispatchPort)proxy.getProxy();
receiving=mock(FbaReceivingStore.class);
when(receiving.currentAgreement(eq(batch),anyString())).thenAnswer(call->confirmed(call.getArgument(1)));
when(receiving.snapshots(anyList())).thenAnswer(call->List.of(new com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.BatchReceiving(batch,List.of(confirmed("101"),confirmed("202")),List.of(),List.of())));
var availability=new AppointmentDispatchAvailability(receiving);
candidates=new AppointmentWorkDispatchCandidates(mapper,appointments,sources,availability);
var target=new AppointmentWorkDispatchBinding(mapper,appointments,sources,changes,receiving,candidates);
org.springframework.test.util.ReflectionTestUtils.setField(target,"allocationReader",new AppointmentDispatchAllocationReader(new AppointmentDispatchAllocationRepository(mapper),sources,availability));
var proxy=new ProxyFactory(target);proxy.setProxyTargetClass(true);proxy.addAdvice(new TransactionInterceptor(manager,new AnnotationTransactionAttributeSource()));binding=(AppointmentWorkDispatchPort)proxy.getProxy();
}
/** 成功场景明确提供本客户原来源的正式收货时间,不借其他客户确认通过。 */
private com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.Agreement confirmed(String customer) {
var details=new FbaAppointmentDetails(null,null,null,"CONF-"+customer,java.time.LocalDateTime.of(2026,10,26,10,0),null,"SUCCESS",null,null,null);
var source=new com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.Source("101".equals(customer)?"ORDER:1":"BOOKING:real-2","FBA:ONT8","101".equals(customer)?80:160,"v1",1,"IN-"+customer);
var member=new com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.Member(batch,"AP-1",0,customer,List.of(source),null);
return new com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.Agreement("RA-"+customer,0,"ACTIVE","FBA:ONT8",details,"America/Los_Angeles",List.of(member),"仓库明确确认");
}
/** 插入真实客户、托盘和逐票数量,不从请求猜测。 */
private void seed(long item,String customer,long order,int count,String destination) {