fix(dispatch): 在审核及拣货前核对预约与运输时间
This commit is contained in:
+8
-2
@@ -454,8 +454,14 @@ public class DispatchAggregatePersistenceTest {
|
||||
f.db.update("INSERT INTO ll_dispatch_appointment_allocation(dispatch_id,dispatch_item_id,plan_pallet_id,batch_key,customer_id,source_key,destination_key,package_count) VALUES(?,?,1,'FBA_APPOINTMENT:900','101','ORDER:9001','FBA:ONT8',3)",dispatch,item);
|
||||
var detail=f.tx.execute(s->f.service.detail(f.actor,original.dispatchNo()));
|
||||
assertThat(detail.orderDetails()).hasSize(2); assertThat(detail.dispatchInfo().appointmentBatches()).hasSize(1);
|
||||
assertThat(detail.dispatchInfo().fbaAppointments().get(0).appointmentNo()).isEqualTo("CONFIRM-REAL");
|
||||
assertThat(detail.dispatchInfo().fbaAppointments().get(0).confirmedAt()).isEqualTo(Instant.parse("2026-10-19T10:00:00Z"));
|
||||
// 批次头写SUCCESS不代表存在当前有效收货确认,不再把旧头时间作为审批依据。
|
||||
assertThat(detail.dispatchInfo().fbaAppointments().get(0).status()).isEqualTo("PENDING");
|
||||
assertThat(detail.dispatchInfo().fbaAppointments().get(0).confirmedAt()).isNull();
|
||||
var provider=mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class);
|
||||
when(provider.reviewEvidence(original.dispatchNo())).thenReturn(List.of(new com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.ReceivingWindow("FBA_APPOINTMENT:900","AP-EXISTING","101","FBA:ONT8","CURRENT-CONFIRM",Instant.parse("2026-10-20T17:00:00Z"),null,"AGR-1",1)));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(f.service,"appointments",new DispatchAppointmentAllocations(provider));
|
||||
var current=f.tx.execute(s->f.service.detail(f.actor,original.dispatchNo())).dispatchInfo().fbaAppointments().get(0);
|
||||
assertThat(current.appointmentNo()).isEqualTo("CURRENT-CONFIRM");assertThat(current.confirmedAt()).isEqualTo(Instant.parse("2026-10-20T17:00:00Z"));
|
||||
var c=f.source(3,"101",503,"ONT8",2);
|
||||
f.tx.executeWithoutResult(s->f.service.addCargo(f.actor,original.dispatchNo(),new DispatchCommands.AddCargo(original.recordVersion(),"add-third",List.of(c),List.of())));
|
||||
var restored=f.tx.execute(s->f.service.create(f.actor,f.create("two-customers",List.of(a,b))));
|
||||
|
||||
+58
@@ -19,6 +19,64 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
/** 真实排车、车次、命令和来源入口回归;未物化PLAN不得进入仅接受实托的执行锁。 */
|
||||
class DispatchLoadingEligibilityPersistenceTest {
|
||||
@org.junit.jupiter.api.BeforeEach void authorize() {
|
||||
org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(new org.springframework.security.authentication.TestingAuthenticationToken("operator","",
|
||||
"platform:overseas-warehouse:dispatch:list","platform:overseas-warehouse:dispatch:edit","platform:overseas-warehouse:dispatch:loading-complete",
|
||||
"platform:fleet:trip:dispatch-review","platform:fleet:trip:loading-review","lastleg:pda:operate","ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR"));
|
||||
}
|
||||
@org.junit.jupiter.api.AfterEach void clearIdentity(){org.springframework.security.core.context.SecurityContextHolder.clearContext();}
|
||||
/** 允许预排;客服提交及两级批准均复验,缺预约时仍能驳回客服而不能开始拣货。 */
|
||||
@org.junit.jupiter.api.Test void preArrangementRemainsAllowedButReviewsCannotApproveUnconfirmedAppointments() throws Exception {
|
||||
var f=new DispatchAggregatePersistenceTest.Fixture();
|
||||
var created=f.tx.execute(t->f.service.create(f.actor,f.configured("review-appointment",List.of(f.source(1,"101",501,"ONT8",3)))));
|
||||
var provider=mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class);
|
||||
ReflectionTestUtils.setField(f.service,"appointments",new DispatchAppointmentAllocations(provider));
|
||||
when(provider.reviewBlockReason(anyString(),any(),any())).thenReturn("预约收货时间尚未确认");
|
||||
assertThatThrownBy(()->f.tx.execute(t->f.service.submitReview(f.actor,created.dispatchNo(),new DispatchCommands.SubmitReview(created.recordVersion(),"blocked-submit",null)))).hasMessageContaining("尚未确认");
|
||||
when(provider.reviewBlockReason(anyString(),any(),any())).thenReturn(null);
|
||||
var submitted=f.tx.execute(t->f.service.submitReview(f.actor,created.dispatchNo(),new DispatchCommands.SubmitReview(created.recordVersion(),"submit",null)));
|
||||
when(provider.reviewBlockReason(anyString(),any(),any())).thenReturn("预约收货时间尚未确认");
|
||||
var gate=f.tx.execute(t->f.service.detail(f.actor,created.dispatchNo())).actions().get(com.baiyue.lastleg.dispatch.port.DispatchTypes.Action.DISPATCH_REVIEW);
|
||||
assertThat(gate.allowed()).isTrue();assertThat(gate.blockCode()).isEqualTo("APPROVAL_BLOCKED");
|
||||
assertThatThrownBy(()->f.tx.execute(t->f.service.review(f.actor,created.dispatchNo(),false,new DispatchCommands.ReviewCommand(submitted.recordVersion(),"bad-approve",com.baiyue.lastleg.dispatch.port.DispatchTypes.Decision.APPROVED,null,null)))).hasMessageContaining("尚未确认");
|
||||
// 预约依据变化后仍可驳回,不让失效审核把客服修正入口锁死。
|
||||
var changedWindow=new com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.ReceivingWindow("FBA_APPOINTMENT:1","YY1","101","FBA:ONT8","CONF1",java.time.Instant.parse("2026-12-01T10:00:00Z"),null,"agreement-1",2);
|
||||
when(provider.reviewEvidence(anyString())).thenReturn(List.of(changedWindow));
|
||||
var rejected=f.tx.execute(t->f.service.review(f.actor,created.dispatchNo(),false,new DispatchCommands.ReviewCommand(submitted.recordVersion(),"reject",com.baiyue.lastleg.dispatch.port.DispatchTypes.Decision.REJECTED,"请客服核对预约",null)));
|
||||
when(provider.reviewBlockReason(anyString(),any(),any())).thenReturn(null);
|
||||
var submittedAgain=f.tx.execute(t->f.service.submitReview(f.actor,created.dispatchNo(),new DispatchCommands.SubmitReview(rejected.recordVersion(),"resubmit",null)));
|
||||
var reviewed=f.tx.execute(t->f.service.review(f.actor,created.dispatchNo(),false,new DispatchCommands.ReviewCommand(submittedAgain.recordVersion(),"dispatch-pass",com.baiyue.lastleg.dispatch.port.DispatchTypes.Decision.APPROVED,null,null)));
|
||||
when(provider.reviewBlockReason(anyString(),any(),any())).thenReturn("预约收货时间尚未确认");
|
||||
assertThatThrownBy(()->f.tx.execute(t->f.service.review(f.actor,created.dispatchNo(),true,new DispatchCommands.ReviewCommand(reviewed.recordVersion(),"bad-loading-approve",com.baiyue.lastleg.dispatch.port.DispatchTypes.Decision.APPROVED,null,null)))).hasMessageContaining("尚未确认");
|
||||
when(provider.reviewBlockReason(anyString(),any(),any())).thenReturn(null);
|
||||
f.tx.execute(t->f.service.review(f.actor,created.dispatchNo(),true,new DispatchCommands.ReviewCommand(reviewed.recordVersion(),"loading-pass",com.baiyue.lastleg.dispatch.port.DispatchTypes.Decision.APPROVED,null,null)));
|
||||
when(provider.reviewEvidence(anyString())).thenReturn(List.of());
|
||||
assertThatThrownBy(()->f.tx.execute(t->{f.service.requireReviewAppointment(f.repository.find(created.dispatchNo()));return null;})).hasMessageContaining("原审核依据失效");
|
||||
when(provider.reviewBlockReason(anyString(),any(),any())).thenReturn("预约收货时间尚未确认");
|
||||
var pda=new DispatchContracts.Actor("picker",ActorMode.PDA,"9");
|
||||
assertThat(f.tx.execute(t->f.service.detail(pda,created.dispatchNo())).actions().get(com.baiyue.lastleg.dispatch.port.DispatchTypes.Action.PICK).allowed()).isFalse();
|
||||
assertThatThrownBy(()->f.tx.execute(t->{f.service.requireReviewAppointment(f.repository.find(created.dispatchNo()));return null;})).hasMessageContaining("尚未确认");
|
||||
}
|
||||
/** 照片或终端不改变预约门槛;显示与正式提交均拒绝未准备好的预约。 */
|
||||
@org.junit.jupiter.api.Test void appointmentBlockIsVisibleBeforeLoadingSubmission() throws Exception {
|
||||
var f=new DispatchAggregatePersistenceTest.Fixture();
|
||||
var created=f.tx.execute(t->f.service.create(f.actor,f.configured("appointment-gate",List.of(f.source(1,"101",501,"ONT8",3)))));
|
||||
var approved=f.approve(created);f.picked(1,"101",501,3);
|
||||
f.db.update("UPDATE ll_dispatch_order SET picked_package_count=3");
|
||||
var provider=mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class);
|
||||
when(provider.departureBlockReason(created.dispatchNo())).thenReturn("请先约仓成功再排车或发车");
|
||||
ReflectionTestUtils.setField(f.service,"appointments",new DispatchAppointmentAllocations(provider));
|
||||
var actor=new DispatchContracts.Actor("picker",ActorMode.PDA,"9");
|
||||
var preview=f.tx.execute(t->f.service.loading(actor,created.dispatchNo()));
|
||||
assertThat(preview.pickingReconciled()).isTrue();
|
||||
assertThat(preview.loadingComplete().allowed()).isFalse();
|
||||
assertThat(preview.loadingComplete().blockCode()).isEqualTo("APPOINTMENT_NOT_READY");
|
||||
assertThat(f.tx.execute(t->f.service.detail(actor,created.dispatchNo())).actions().get(com.baiyue.lastleg.dispatch.port.DispatchTypes.Action.LOADING_COMPLETE).allowed()).isFalse();
|
||||
assertThatThrownBy(()->f.tx.execute(t->f.service.loadingComplete(actor,created.dispatchNo(),new DispatchCommands.LoadingComplete(approved.recordVersion(),"blocked-loading",preview.manifestHash(),null))))
|
||||
.hasMessageContaining("先约仓成功");
|
||||
assertThat(f.repository.find(created.dispatchNo()).getStage()).isEqualTo("LOADING");
|
||||
assertThat(f.repository.find(created.dispatchNo()).getRecordVersion()).isEqualTo(approved.recordVersion());
|
||||
}
|
||||
/** 全车在途和部分实托已拣的混合车均应业务拒绝,原单/命令/占用/扫码事实完全不变。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans={false,true})
|
||||
|
||||
+4
@@ -31,6 +31,10 @@ class DispatchPdaReadPersistenceTest {
|
||||
var approved=f.tx.execute(s->f.service.review(f.actor,created.dispatchNo(),false,new DispatchCommands.ReviewCommand(submitted.recordVersion(),"dispatch",Decision.APPROVED,null,null)));
|
||||
var list=f.tx.execute(s->queries.list(actor,null));assertThat(list).hasSize(1);
|
||||
var task=list.get(0);assertThat(task.stage()).isEqualTo(Stage.BEFORE_LOADING);
|
||||
var platform=f.tx.execute(s->f.service.detail(f.actor,created.dispatchNo()));
|
||||
assertThat(task.plannedDepartureAt()).isEqualTo(platform.dispatchInfo().plannedDepartureAt());
|
||||
assertThat(task.estimatedArrivalAt()).isEqualTo(platform.dispatchInfo().estimatedArrivalAt());
|
||||
assertThat(task.fbaAppointments()).isEqualTo(platform.dispatchInfo().fbaAppointments());
|
||||
assertThat(task.actions().get(Action.LOADING_REVIEW).allowed()).isTrue();assertThat(task.actions().get(Action.PICK).allowed()).isFalse();
|
||||
assertThat(task.resources().vehicle().plate()).isEqualTo("TEST-TRUCK");assertThat(task.resources().driver().name()).isEqualTo("测试司机");
|
||||
f.tx.execute(s->f.service.review(actor,created.dispatchNo(),true,new DispatchCommands.ReviewCommand(approved.recordVersion(),"tally-reject",Decision.REJECTED,"车辆不符",null)));
|
||||
|
||||
+11
@@ -23,6 +23,17 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
/** 根版本、幂等、真实客户作业与扫码持久化;库存/归属应用命令在此明确模拟,另由原域直接测试验证。 */
|
||||
class DispatchPickingPersistenceTest {
|
||||
/** 即使旧记录已经审核通过,真实扫码仍复验预约;拒绝时不物化、不扣库存。 */
|
||||
@Test void unconfirmedAppointmentBlocksPickingBeforeInventoryWrites() throws Exception {
|
||||
var fixture=new Picking(true);fixture.receivedPlans(9,11);
|
||||
var provider=mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class);
|
||||
when(provider.reviewBlockReason(anyString(),any(),any())).thenReturn("预约收货时间尚未确认");
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(fixture.f.service,"appointments",new DispatchAppointmentAllocations(provider));
|
||||
assertThatThrownBy(()->fixture.f.tx.execute(s->fixture.service.pick(fixture.actor,fixture.number,fixture.item,
|
||||
new DispatchCommands.Pick(fixture.version,"appointment-blocked",11L,9,0,null,null)))).hasMessageContaining("尚未确认");
|
||||
verify(fixture.inventory,never()).ship(any(),any(),any());
|
||||
assertThat(fixture.f.db.queryForList("SELECT package_count FROM ll_dispatch_item ORDER BY customer_id",Integer.class)).containsExactly(10,10);
|
||||
}
|
||||
/** 数量可自然同步不代表能接管其他车的实际托盘。 */
|
||||
@Test void receivedPalletClaimedByAnotherDispatchCannotBePicked() throws Exception {
|
||||
var fixture=new Picking(true);fixture.receivedPlans(9,11);
|
||||
|
||||
+11
@@ -58,6 +58,16 @@ class AppointmentWorkDispatchBindingTest {
|
||||
when(mapper.pickedPhysical(30,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",null,4)));
|
||||
when(sources.canonicalOrders(anyList())).thenReturn(List.of(new AppointmentCoverageService.SourceRef("C","ORDER:1",null)));
|
||||
assertDoesNotThrow(()->service.requireDeparture("D-20"));
|
||||
when(mapper.physical(10,"C")).thenReturn(List.of(new AppointmentWorkDispatchMapper.Physical(1L,"C","FBA:ONT8",4,4)));
|
||||
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());
|
||||
assertEquals(arrival,service.reviewEvidence("D-20").get(0).startAt(),"10点洛杉矶预约应按当地时区解释");
|
||||
assertNotNull(service.reviewBlockReason("D-20",departure,null));
|
||||
assertNotNull(service.reviewBlockReason("D-20",arrival.plusSeconds(60),arrival.plusSeconds(120)));
|
||||
assertNotNull(service.reviewBlockReason("D-20",departure,arrival.plusSeconds(60)));
|
||||
when(item.packageCount()).thenReturn(5);when(mapper.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",20)).thenReturn(1L);
|
||||
assertTrue(service.departureBlockReason("D-20").contains("收货确认未完整覆盖")); // Batch has 5, but the current agreement covers only 4 across every dispatch.
|
||||
when(item.packageCount()).thenReturn(4);when(mapper.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",20)).thenReturn(0L);
|
||||
@@ -76,6 +86,7 @@ class AppointmentWorkDispatchBindingTest {
|
||||
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.requireDeparture("D-20"));
|
||||
when(receiving.currentAgreement("FBA_APPOINTMENT:1","C")).thenReturn(null);
|
||||
assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.requireDeparture("D-20"));
|
||||
assertTrue(service.reviewBlockReason("D-20",departure,arrival).contains("尚无有效收货确认"));
|
||||
verifyNoInteractions(changes);
|
||||
}
|
||||
private com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.Agreement agreement(String customer,int packages) {
|
||||
|
||||
Reference in New Issue
Block a user