优化
This commit is contained in:
+59
-10
@@ -3,8 +3,9 @@ package com.baiyue.lastleg.dispatch.service;
|
||||
import com.baiyue.lastleg.dispatch.mapper.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.service.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.gateway.OwnedBusinessFileGateway;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.BindPhoto;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.PhotoSlot;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.port.DispatchLoadingPhotoPort.BindPhoto;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.port.DispatchLoadingPhotoPort.Documents;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.port.DispatchLoadingPhotoPort.Slot;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.mapper.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.repository.OwnedBusinessRepository;
|
||||
import java.nio.file.*;
|
||||
@@ -42,7 +43,7 @@ class DispatchLoadingDeparturePersistenceTest {
|
||||
assertThat(f.f.db.queryForObject("SELECT COUNT(*) FROM ll_delivery_order",Long.class)).isZero();
|
||||
var fifth=f.bind(4,new Actor("picker",ActorMode.PDA,"9"));assertThat(fifth.stage()).isEqualTo("DELIVERING");
|
||||
assertThat(fifth.complete()).isTrue();assertThat(fifth.photos()).hasSize(5);
|
||||
var replayPhoto=f.f.tx.execute(s->f.documents.bind(new Actor("picker",ActorMode.PDA,"9"),f.number,PhotoSlot.RECEIPT,new BindPhoto("4","photo-bind-4","1004")));
|
||||
var replayPhoto=f.f.tx.execute(s->f.documents.bind(new Actor("picker",ActorMode.PDA,"9"),f.number,Slot.RECEIPT,new BindPhoto("4","photo-bind-4","1004")));
|
||||
assertThat(replayPhoto).isEqualTo(fifth);
|
||||
assertThat(result.counts().shippedPackageCount()).isZero();assertThat(result.result()).isInstanceOf(DispatchResults.Loaded.class);
|
||||
assertThat(f.f.db.queryForList("SELECT status FROM ll_outbound_order",String.class)).containsExactly("OUTBOUNDED","OUTBOUNDED");
|
||||
@@ -103,13 +104,16 @@ class DispatchLoadingDeparturePersistenceTest {
|
||||
verify(f.delivery,never()).prepareForDeparture(any(),any(),anyString());verify(f.sourceWorkflow,never()).departWholeSources(any(),any(),any(),any());
|
||||
}
|
||||
|
||||
/** 资料上传必须在装车后且属于绑定仓,平台只读权限不能保存照片。 */
|
||||
/** 装车中可上传,装车前与跨仓拒绝,平台只读权限不能保存照片。 */
|
||||
@Test void loadingDocumentsEnforceStageWarehouseAndWritePermission() throws Exception {
|
||||
authorize();var f=new Fixture();
|
||||
assertThatThrownBy(()->f.documents.get(f.f.actor,f.number)).hasMessageContaining("装车完成后");
|
||||
assertThat(f.documents.get(f.f.actor,f.number).stage()).isEqualTo("LOADING");
|
||||
f.f.db.update("UPDATE ll_dispatch_order SET stage='BEFORE_LOADING'");
|
||||
assertThatThrownBy(()->f.documents.get(f.f.actor,f.number)).hasMessageContaining("进入装车中后");
|
||||
f.f.db.update("UPDATE ll_dispatch_order SET stage='LOADING'");
|
||||
f.f.tx.execute(s->f.service.complete(f.f.actor,f.number,f.request("complete")));
|
||||
assertThatThrownBy(()->f.documents.get(new Actor("picker",ActorMode.PDA,"99"),f.number)).hasMessageContaining("当前绑定仓");
|
||||
assertThatThrownBy(()->f.f.tx.execute(s->f.documents.bind(f.f.actor,f.number,PhotoSlot.FUEL_START,new BindPhoto("0","wrong-slot","1000")))).hasMessageContaining("装车照片框");
|
||||
assertThatThrownBy(()->f.f.tx.execute(s->f.documents.bind(f.f.actor,f.number,null,new BindPhoto("0","wrong-slot","1000")))).hasMessageContaining("装车照片框");
|
||||
SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("reader","","platform:overseas-warehouse:dispatch:list"));
|
||||
assertThat(f.documents.get(f.f.actor,f.number).editable()).isFalse();
|
||||
assertThatThrownBy(()->f.bind(0,f.f.actor)).hasMessageContaining("无装车资料操作权限");
|
||||
@@ -124,7 +128,7 @@ class DispatchLoadingDeparturePersistenceTest {
|
||||
var results=new ArrayList<Future<Boolean>>();
|
||||
for(String key:List.of("admin-last","pda-last"))results.add(workers.submit(()->{
|
||||
authorize();try {start.await(5,TimeUnit.SECONDS);var actor=key.startsWith("pda")?new Actor("picker",ActorMode.PDA,"9"):f.f.actor;
|
||||
f.f.tx.execute(s->f.documents.bind(actor,f.number,PhotoSlot.RECEIPT,new BindPhoto("4",key,"1004")));return true;
|
||||
f.f.tx.execute(s->f.documents.bind(actor,f.number,Slot.RECEIPT,new BindPhoto("4",key,"1004")));return true;
|
||||
}catch(com.baiyue.common.core.exception.BizException expected){return false;}
|
||||
finally {SecurityContextHolder.clearContext();}
|
||||
}));
|
||||
@@ -135,6 +139,50 @@ class DispatchLoadingDeparturePersistenceTest {
|
||||
assertThat(f.f.repository.find(f.number).getStage()).isEqualTo("DELIVERING");
|
||||
}
|
||||
|
||||
/** 资料先齐不能提前发车;确认装车后直接派送,原键返回相同最终结果。 */
|
||||
@Test void photosReadyBeforeLoadingConfirmationDepartAndReplayOnce() throws Exception {
|
||||
authorize();var f=new Fixture();
|
||||
for(int i=0;i<5;i++)assertThat(f.bind(i,f.f.actor).stage()).isEqualTo("LOADING");
|
||||
verify(f.delivery,never()).prepareForDeparture(any(),any(),anyString());
|
||||
var result=f.f.tx.execute(s->f.service.complete(f.f.actor,f.number,f.request("ready-complete")));
|
||||
assertThat(result.action()).isEqualTo(Action.LOADING_COMPLETE);
|
||||
assertThat(result.stage()).isEqualTo(Stage.DELIVERING);assertThat(result.counts().shippedPackageCount()).isEqualTo(8);
|
||||
assertThat(result.result()).isInstanceOf(DispatchResults.Loaded.class);
|
||||
var replay=f.f.tx.execute(s->f.service.complete(f.f.actor,f.number,f.request("ready-complete")));
|
||||
assertThat(replay.replayed()).isTrue();assertThat(replay.stage()).isEqualTo(Stage.DELIVERING);
|
||||
assertThat(replay.recordVersion()).isEqualTo(result.recordVersion());assertThat(replay.result()).isEqualTo(result.result());
|
||||
assertThat(f.f.tx.execute(s->f.f.service.command(f.f.actor,f.number,"ready-complete")).stage()).isEqualTo(Stage.DELIVERING);
|
||||
verify(f.delivery,times(1)).prepareForDeparture(any(),any(),anyString());
|
||||
verify(f.sourceWorkflow,times(1)).departWholeSources(any(),any(),any(),any());
|
||||
}
|
||||
|
||||
/** 资料先齐时派送失败回滚本次装车确认,五张已保存照片保留供原请求重试。 */
|
||||
@Test void readyDocumentsSurviveFailedLoadingAndOriginalRequestCanRetry() throws Exception {
|
||||
authorize();var f=new Fixture();for(int i=0;i<5;i++)f.bind(i,f.f.actor);
|
||||
doThrow(new IllegalStateException("模拟发运失败")).when(f.sourceWorkflow).departWholeSources(any(),any(),any(),any());
|
||||
assertThatThrownBy(()->f.f.tx.execute(s->f.service.complete(f.f.actor,f.number,f.request("retry-ready")))).hasMessageContaining("模拟发运失败");
|
||||
assertThat(f.f.repository.find(f.number).getStage()).isEqualTo("LOADING");
|
||||
assertThat(f.documents.get(f.f.actor,f.number).complete()).isTrue();
|
||||
assertThat(f.f.db.queryForObject("SELECT COUNT(*) FROM ll_delivery_order",Long.class)).isZero();
|
||||
assertThat(f.f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_command WHERE idempotency_key='retry-ready'",Long.class)).isZero();
|
||||
doNothing().when(f.sourceWorkflow).departWholeSources(any(),any(),any(),any());
|
||||
assertThat(f.f.tx.execute(s->f.service.complete(f.f.actor,f.number,f.request("retry-ready"))).stage()).isEqualTo(Stage.DELIVERING);
|
||||
}
|
||||
|
||||
/** 最后一张照片与装车确认同时提交,不论谁先拿父锁均只发车一次。 */
|
||||
@Test void lastPhotoAndLoadingConfirmationCanFinishConcurrently() throws Exception {
|
||||
authorize();var f=new Fixture();for(int i=0;i<4;i++)f.bind(i,f.f.actor);
|
||||
var start=new CountDownLatch(1);var workers=Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
var load=workers.submit(()->f.tryComplete(start,f.f.actor,"concurrent-load"));
|
||||
var photo=workers.submit(()->{authorize();try {start.await(5,TimeUnit.SECONDS);return f.bind(4,new Actor("picker",ActorMode.PDA,"9"));}finally{SecurityContextHolder.clearContext();}});
|
||||
start.countDown();assertThat(load.get(25,TimeUnit.SECONDS)).isTrue();assertThat(photo.get(25,TimeUnit.SECONDS)).isNotNull();
|
||||
} finally {workers.shutdownNow();}
|
||||
assertThat(f.f.repository.find(f.number).getStage()).isEqualTo("DELIVERING");
|
||||
verify(f.delivery,times(1)).prepareForDeparture(any(),any(),anyString());
|
||||
verify(f.sourceWorkflow,times(1)).departWholeSources(any(),any(),any(),any());
|
||||
}
|
||||
|
||||
/** 配置资料与装车权限及PDA既有角色,不授予人工派送、发车权限。 */
|
||||
private static void authorize(){SecurityContextHolder.getContext().setAuthentication(new TestingAuthenticationToken("operator","",
|
||||
"platform:overseas-warehouse:dispatch:list","platform:overseas-warehouse:dispatch:edit","platform:overseas-warehouse:dispatch:loading-complete","lastleg:pda:operate","ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR"));}
|
||||
@@ -176,7 +224,6 @@ class DispatchLoadingDeparturePersistenceTest {
|
||||
var hashes=new DispatchCommandHasher(f.json);
|
||||
var sources=new DispatchSourceRepository(mock(DispatchCandidateMapper.class),mock(ReturnDispatchCandidateMapper.class),f.session.getMapper(DispatchSourceMapper.class));
|
||||
var departure=new DispatchDepartureService(f.service,f.repository,sources,delivery,sourceWorkflow,f.fleet,ledger,hashes,f.json);
|
||||
service=new DispatchLoadingDepartureService(f.service,delivery,departure,ledger,hashes);
|
||||
f.session.getConfiguration().addMapper(OwnedBusinessMapper.class);
|
||||
String schema=Files.readString(Path.of("src/main/resources/db/lastleg.sql"));
|
||||
for(String suffix:List.of("record","command","history")) {
|
||||
@@ -187,10 +234,12 @@ class DispatchLoadingDeparturePersistenceTest {
|
||||
var records=new OwnedBusinessRepository(f.session.getMapper(OwnedBusinessMapper.class),mock(OwnedBusinessQueryMapper.class));
|
||||
var codec=new OwnedBusinessSupport(f.json);var files=mock(OwnedBusinessFileGateway.class);
|
||||
when(files.metadata(anyLong(),any(),anyLong())).thenAnswer(call->new OwnedBusinessFileGateway.Evidence(call.getArgument(2),"loading.png","image/png",12,"2026-10-21T00:00:00Z"));
|
||||
documents=new DispatchLoadingDocumentService(f.service,service,records,new OwnedBusinessCommandService(records,codec),new OwnedBusinessProjection(codec),codec,files);
|
||||
var photos=new DispatchLoadingPhotoAdapter(records,new OwnedBusinessCommandService(records,codec),new OwnedBusinessProjection(codec),codec,files);
|
||||
service=new DispatchLoadingDepartureService(f.service,delivery,departure,ledger,hashes,photos);
|
||||
documents=new DispatchLoadingDocumentService(f.service,service,photos);
|
||||
}
|
||||
/** 每张照片保留独立版本与幂等键,复用真实附加记录保存事务。 */
|
||||
DispatchLoadingDocumentService.Documents bind(int index,Actor actor) {
|
||||
Documents bind(int index,Actor actor) {
|
||||
return f.tx.execute(s->documents.bind(actor,number,DispatchLoadingDocumentService.SLOTS.get(index),new BindPhoto(Integer.toString(index),"photo-bind-"+index,Long.toString(1000L+index))));
|
||||
}
|
||||
/** 每次请求沿用最初真实测量和版本,便于验证竞争及原键恢复。 */
|
||||
|
||||
+52
-1
@@ -23,6 +23,34 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
/** 根版本、幂等、真实客户作业与扫码持久化;库存/归属应用命令在此明确模拟,另由原域直接测试验证。 */
|
||||
class DispatchPickingPersistenceTest {
|
||||
/** 预排两托10+10、实收9+11,首次扫码同步真实数量并保留原审核,原键重放不重复扣货。 */
|
||||
@Test void receivedPalletCountsReplaceEstimatesAtomically() throws Exception {
|
||||
var fixture=new Picking(true);fixture.receivedPlans(9,11);
|
||||
var firstRequest=new DispatchCommands.Pick(fixture.version,"received-nine",11L,9,0,null,null);
|
||||
var first=fixture.f.tx.execute(s->fixture.service.pick(fixture.actor,fixture.number,fixture.item,firstRequest));
|
||||
assertThat(fixture.f.db.queryForList("SELECT package_count FROM ll_dispatch_item ORDER BY customer_id",Integer.class)).containsExactly(9,11);
|
||||
assertThat(fixture.f.db.queryForObject("SELECT package_count FROM ll_dispatch_order",Integer.class)).isEqualTo(20);
|
||||
assertThat(fixture.f.db.queryForObject("SELECT review_state FROM ll_dispatch_order",String.class)).isEqualTo("APPROVED");
|
||||
var replay=fixture.f.tx.execute(s->fixture.service.pick(fixture.actor,fixture.number,fixture.item,firstRequest));
|
||||
assertThat(replay.replayed()).isTrue();
|
||||
var second=fixture.f.tx.execute(s->fixture.service.pick(fixture.actor,fixture.number,fixture.secondItem,
|
||||
new DispatchCommands.Pick(first.recordVersion(),"received-eleven",12L,11,0,null,null)));
|
||||
assertThat(((DispatchResults.Picked)second.result()).pickingReconciled()).isTrue();
|
||||
verify(fixture.inventory,times(2)).ship(anyString(),eq("picker"),any());
|
||||
assertThat(fixture.f.db.queryForObject("SELECT SUM(available_packages) FROM ll_inventory_position",Long.class)).isZero();
|
||||
}
|
||||
|
||||
/** 实收绑定后扣货失败必须同时回滚数量、绑定、总数和原审核。 */
|
||||
@Test void receivedQuantityAndBindingRollBackWhenStockWriteFails() throws Exception {
|
||||
var fixture=new Picking(true);fixture.receivedPlans(8,11);
|
||||
doThrow(new IllegalStateException("库存命令失败")).when(fixture.inventory).ship(anyString(),anyString(),any());
|
||||
assertThatThrownBy(()->fixture.f.tx.execute(s->fixture.service.pick(fixture.actor,fixture.number,fixture.item,
|
||||
new DispatchCommands.Pick(fixture.version,"receipt-rollback",11L,8,0,null,null)))).isInstanceOf(IllegalStateException.class);
|
||||
assertThat(fixture.f.db.queryForList("SELECT package_count FROM ll_dispatch_item ORDER BY customer_id",Integer.class)).containsExactly(10,10);
|
||||
assertThat(fixture.f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_item WHERE warehouse_pallet_id IS NOT NULL",Long.class)).isZero();
|
||||
assertThat(fixture.f.db.queryForObject("SELECT package_count FROM ll_dispatch_order",Integer.class)).isEqualTo(20);
|
||||
assertThat(fixture.f.db.queryForObject("SELECT record_version FROM ll_dispatch_order",Long.class)).isEqualTo(fixture.version);
|
||||
}
|
||||
/** 原键在后续版本仍恢复原结果,不能重复调用SHIP或创建第二条真实扫码。 */
|
||||
@Test void sameKeyShipsOnceAndLastPickDoesNotConfirmLoading() throws Exception {
|
||||
var fixture=new Picking();var request=new DispatchCommands.Pick(fixture.version,"pda-pick-original",11L,3,6,null,"实际扫码");
|
||||
@@ -68,6 +96,7 @@ class DispatchPickingPersistenceTest {
|
||||
final DispatchAggregatePersistenceTest.Fixture f=new DispatchAggregatePersistenceTest.Fixture();
|
||||
final InventoryCommandService inventory=mock(InventoryCommandService.class);final DispatchPickOrderAttributionService attribution=mock(DispatchPickOrderAttributionService.class);
|
||||
final DispatchPickingService service;final String number;final long item,version;final Long secondItem;
|
||||
final DispatchSourceRepository sources;
|
||||
final DispatchContracts.Actor actor=new DispatchContracts.Actor("picker",ActorMode.PDA,"9");
|
||||
/** 建立真实客户来源、作业表、扫码表与可以参与回滚的库存行。 */
|
||||
Picking() throws Exception {this(false);}
|
||||
@@ -94,10 +123,32 @@ class DispatchPickingPersistenceTest {
|
||||
doAnswer(call->{com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move move=call.getArgument(2);f.db.update("UPDATE ll_inventory_position SET available_packages=available_packages-?,version=version+1 WHERE id=? AND customer_id=?",move.packages(),move.positionId(),call.getArgument(0));return null;}).when(inventory).ship(anyString(),eq("picker"),any());
|
||||
doAnswer(call->{String customer=call.getArgument(0);DispatchPdaPick pick=call.getArgument(1);long order="101".equals(customer)?9001L:9002L;long receipt="101".equals(customer)?1L:2L;f.db.update("INSERT INTO ll_dispatch_pda_pick_order(id,customer_id,pick_id,order_id,receipt_id,packages,product_quantity)VALUES(?,?,?,?,?,?,?)",pick.getId(),customer,pick.getId(),order,receipt,pick.getPackages(),pick.getProductQuantity());return List.of(new DispatchPickOrderAttributionService.OrderPackages(order,pick.getPackages()));})
|
||||
.when(attribution).attribute(anyString(),any(DispatchPdaPick.class),isNull());
|
||||
var sources=new DispatchSourceRepository(mock(DispatchCandidateMapper.class),mock(ReturnDispatchCandidateMapper.class),f.session.getMapper(DispatchSourceMapper.class));
|
||||
sources=spy(new DispatchSourceRepository(mock(DispatchCandidateMapper.class),mock(ReturnDispatchCandidateMapper.class),f.session.getMapper(DispatchSourceMapper.class)));
|
||||
var commands=new DispatchCommandLedger(new DispatchCommandRepository(f.session.getMapper(DispatchCommandMapper.class)),new DispatchCommandCodec(f.json));
|
||||
service=new DispatchPickingService(f.service,f.repository,sources,new DispatchPickingRepository(session.getMapper(DispatchPdaTaskMapper.class),session.getMapper(DispatchPdaPickMapper.class),session.getMapper(InventoryPositionMapper.class),session.getMapper(DispatchItemMapper.class)),attribution,inventory,mock(ReturnRelabelDispatchStock.class),commands,new DispatchCommandHasher(f.json),f.json);
|
||||
for(var field:List.of("physicalClaims","relabelSources","sourceRules"))org.springframework.test.util.ReflectionTestUtils.setField(service,field,org.springframework.test.util.ReflectionTestUtils.getField(f.service,field));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service,"fleet",f.fleet);
|
||||
}
|
||||
/** 来源查询在此隔离,实收同步仍使用真实主单、明细、库存及车次事务。 */
|
||||
void receivedPlans(int first,int second) {
|
||||
f.db.update("UPDATE ll_dispatch_item SET warehouse_pallet_id=NULL,source_stage='PLAN',package_count=10");
|
||||
f.db.update("UPDATE ll_dispatch_order SET package_count=20");
|
||||
var rules=(DispatchService)org.springframework.test.util.ReflectionTestUtils.getField(service,"sourceRules");
|
||||
for(int n=1;n<=2;n++) {
|
||||
long plan=n,pallet=500+n;String customer=n==1?"101":"102";int count=n==1?first:second;
|
||||
f.candidates.get(plan).packageCount(count);
|
||||
f.db.update("UPDATE ll_inventory_position SET available_packages=? WHERE id=?",count,10+n);
|
||||
doReturn(List.of(f.candidates.get(plan))).when(sources).selectedPlan(customer,plan);
|
||||
doReturn(List.of(9000+plan)).when(sources).sourceOrders(customer,null,plan);
|
||||
doReturn(List.of(9000+plan)).when(sources).sourceOrders(customer,pallet,plan);
|
||||
doReturn(plan).when(sources).lockPlan(customer,plan);
|
||||
doReturn(1).when(sources).materialize(anyLong(),eq(customer),eq(plan),eq(pallet),anyLong(),any());
|
||||
}
|
||||
when(rules.selectionGroup(anyString(),any())).thenAnswer(call->{
|
||||
var selected=(DispatchCommands.CargoSelection)call.getArgument(1);long plan=selected.planPalletId();var row=f.candidates.get(plan);
|
||||
return new DispatchPlanningModels.SelectionGroup("group","version",List.of(new DispatchPlanningModels.SelectionGroupSource(
|
||||
selected,"PLAN:"+plan,row.customerId(),row.customerName(),"INBOUND_PLAN","PLAN-"+plan,row.packageCount())),List.of(),true,List.of());
|
||||
});
|
||||
}
|
||||
/** 使用实际实体属性建最小测试表,不修改或声称验证正式DDL。 */
|
||||
private static String sqlType(Class<?> type){return type==Long.class?"BIGINT":type==Integer.class?"INT":type==LocalDateTime.class?"TIMESTAMP(3)":"VARCHAR(20000)";}
|
||||
|
||||
+33
-6
@@ -28,15 +28,23 @@ class DispatchPlanPdaReadPersistenceTest {
|
||||
verify(f.sources,never()).materialize(anyLong(),anyString(),anyLong(),anyLong(),anyLong(),any());
|
||||
verify(f.rules,never()).lockExecutionSources(anyString(),anyList());
|
||||
}
|
||||
/** 在途、预留托盘和实收数量漂移均不成为可拣库存。 */
|
||||
/** 在途和来源身份变化均不成为可拣库存。 */
|
||||
@Test void unreceivedOrChangedPlanHasNoPositions() throws Exception {
|
||||
var f=new Fixture();var row=f.f.candidates.get(1L);
|
||||
row.sourceStage("PLAN");assertThat(f.read().items().get(0).positions()).isEmpty();
|
||||
row.sourceStage("RECEIVED");row.packageCount(2);assertThat(f.read().items().get(0).positions()).isEmpty();
|
||||
row.packageCount(3);row.overseasWarehouseId("99");assertThat(f.read().items().get(0).positions()).isEmpty();
|
||||
row.sourceStage("RECEIVED");row.overseasWarehouseId("99");assertThat(f.read().items().get(0).positions()).isEmpty();
|
||||
row.overseasWarehouseId("9");row.customerId("other");assertThat(f.read().items().get(0).positions()).isEmpty();
|
||||
verify(f.sources,never()).stock(anyString(),anyLong(),anyBoolean());
|
||||
}
|
||||
/** 自动预分配的10+10不约束实收9+11;查询给出实际数量且不改数据库。 */
|
||||
@Test void plannedTenAndTenProjectsReceivedNineAndEleven() throws Exception {
|
||||
var f=new Fixture(true);var before=f.dbState();
|
||||
var task=f.read();
|
||||
assertThat(task.items()).extracting(DispatchViews.PdaItem::scheduledPackages).containsExactly(9L,11L);
|
||||
assertThat(task.items()).allSatisfy(i->assertThat(i.positions()).isNotEmpty());
|
||||
assertThat(task.counts().packageCount()).isEqualTo(20);
|
||||
assertThat(f.dbState()).isEqualTo(before);
|
||||
}
|
||||
/** 原计划不能吸收新的订单或未经原车审核的同托来源。 */
|
||||
@Test void changedOrdersOrAdditionalPalletSourceBlocksFirstPick() throws Exception {
|
||||
var f=new Fixture();
|
||||
@@ -65,9 +73,13 @@ class DispatchPlanPdaReadPersistenceTest {
|
||||
final DispatchService rules=mock(DispatchService.class);
|
||||
final DispatchPdaQueryService queries;final String number;
|
||||
/** 原车已审核,只清除自然收货尚未补写的实托字段。 */
|
||||
Fixture() throws Exception {
|
||||
var selection=f.source(1,"101",501,"ONT8",3);
|
||||
var created=f.tx.execute(s->f.service.create(f.actor,f.configured("plan-pda-read",List.of(selection))));
|
||||
Fixture() throws Exception {this(false);}
|
||||
/** 同一客户两托先预排再实收,模拟计划拆分与真实入托数量不同。 */
|
||||
Fixture(boolean twoPallets) throws Exception {
|
||||
var selection=f.source(1,"101",501,"ONT8",twoPallets?10:3);
|
||||
var selections=new java.util.ArrayList<DispatchCommands.CargoSelection>();selections.add(selection);
|
||||
if(twoPallets)selections.add(f.source(2,"101",502,"ONT8",10));
|
||||
var created=f.tx.execute(s->f.service.create(f.actor,f.configured("plan-pda-read",selections)));
|
||||
f.approve(created);number=created.dispatchNo();
|
||||
f.db.update("UPDATE ll_dispatch_item SET warehouse_pallet_id=NULL,source_stage='PLAN'");
|
||||
f.db.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR(64),balance_id BIGINT,pallet_id BIGINT,overseas_warehouse_id VARCHAR(64),available_packages BIGINT,hold_packages BIGINT,allocated_packages BIGINT,picked_packages BIGINT)");
|
||||
@@ -82,6 +94,21 @@ class DispatchPlanPdaReadPersistenceTest {
|
||||
when(rules.selectionGroup(anyString(),any())).thenReturn(new DispatchPlanningModels.SelectionGroup(
|
||||
"group","version",List.of(new DispatchPlanningModels.SelectionGroupSource(
|
||||
selection,"PLAN:1","101","测试客户","INBOUND_PLAN","PLAN-1",3)),List.of(),true,List.of()));
|
||||
if(twoPallets) {
|
||||
for(int n=1;n<=2;n++) {
|
||||
long plan=n,pallet=500+n;int count=n==1?9:11;
|
||||
f.candidates.get(plan).packageCount(count);
|
||||
when(sources.selectedPlan("101",plan)).thenReturn(List.of(f.candidates.get(plan)));
|
||||
when(sources.sourceOrders("101",null,plan)).thenReturn(List.of(9000+plan));
|
||||
when(sources.sourceOrders("101",pallet,plan)).thenReturn(List.of(9000+plan));
|
||||
when(sources.stock("101",pallet,false)).thenReturn(List.of(new Stock(10+n,20+n,pallet,"9",count,0,0,0,1)));
|
||||
}
|
||||
when(rules.selectionGroup(anyString(),any())).thenAnswer(call->{
|
||||
var selected=(DispatchCommands.CargoSelection)call.getArgument(1);long plan=selected.planPalletId();
|
||||
return new DispatchPlanningModels.SelectionGroup("group","version",List.of(new DispatchPlanningModels.SelectionGroupSource(
|
||||
selected,"PLAN:"+plan,"101","测试客户","INBOUND_PLAN","PLAN-"+plan,plan==1?9:11)),List.of(),true,List.of());
|
||||
});
|
||||
}
|
||||
}
|
||||
/** 读取正式PDA入口。 */
|
||||
DispatchViews.PdaTask read(){return f.tx.execute(s->queries.task(new DispatchContracts.Actor("picker",ActorMode.PDA,"9"),number));}
|
||||
|
||||
Reference in New Issue
Block a user