Files
baiyue-firstleg/firstleg/src/test/java/com/baiyue/firstleg/service/DropshipPalletizationServiceTest.java
T
2026-10-19 16:41:20 +08:00

228 lines
17 KiB
Java

package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.CountCartonRequest;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.DiscrepancyAction;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.CountSkuLine;
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.UnsealPalletRequest;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.LabelRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.CartonRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PalletRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.TaskRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.LabelSkuRow;
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.CartonSkuRow;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class DropshipPalletizationServiceTest {
private final FirstlegDocumentNumberGenerator numbers=mock(FirstlegDocumentNumberGenerator.class);
private final DropshipPalletizationMapper mapper=mock(DropshipPalletizationMapper.class);
private final DropshipPalletizationService service=new DropshipPalletizationService();
private final com.baiyue.firstleg.repository.DropshipWorkspaceRepository workspace=mock(com.baiyue.firstleg.repository.DropshipWorkspaceRepository.class);
/** 使用正式仓储入口的等价锁定查询,保留既有扫码测试的事实准备。 */
DropshipPalletizationServiceTest(){ ReflectionTestUtils.setField(service,"mapper",mapper); ReflectionTestUtils.setField(service,"documentNumbers",numbers); ReflectionTestUtils.setField(service,"workspaceRepository",workspace); when(workspace.lockPallet(anyString())).thenAnswer(call -> mapper.palletByNo(call.getArgument(0))); when(workspace.compatible(anyLong(),anyLong())).thenReturn(true); when(mapper.labelSkus(anyLong(),anyLong())).thenAnswer(call -> java.util.List.of(labelSku())); when(mapper.cartonSkus(anyLong())).thenAnswer(call -> java.util.List.of(cartonSku())); }
/** 跨客户预览不领取任务,不写入清点事实。 */
@Test void workspaceResolvesPendingCustomerWithoutMutating(){
ReflectionTestUtils.setField(service,"workspaceRepository",workspace);
TaskRow other=task(); other.setTenantId(2L); other.setTaskStatus("PENDING"); other.setAssigneeId(null);
when(workspace.taskIdsForBox(100L,"BOX-1")).thenReturn(java.util.List.of(10L));
when(mapper.task(10L,100L)).thenReturn(other);
when(mapper.label(10L,"BOX-1")).thenReturn(label());
when(mapper.palletCountByInbound(2L,30L)).thenReturn(other.getExpectedPalletCount());
var result=service.resolveWorkspaceCarton(100L," BOX-1 ",7L);
assertThat(result.task().tenantId()).isEqualTo(2L);
assertThat(result.carton().canCount()).isTrue();
verify(mapper,never()).claim(anyLong(),anyLong(),anyLong(),anyString());
verify(mapper,never()).addCounted(anyLong(),anyLong(),anyInt(),anyInt());
}
/** 不允许从跨票扫码接管其他作业员的任务。 */
@Test void workspaceRejectsOtherOperatorsTaskAndAmbiguousBox(){
ReflectionTestUtils.setField(service,"workspaceRepository",workspace);
when(workspace.taskIdsForBox(100L,"BOX-1")).thenReturn(java.util.List.of(10L));
TaskRow other=task(); other.setAssigneeId(9L); when(mapper.task(10L,100L)).thenReturn(other);
assertThatThrownBy(()->service.resolveWorkspaceCarton(100L,"BOX-1",7L)).isInstanceOf(BizException.class).hasMessageContaining("其他人员");
when(workspace.taskIdsForBox(100L,"BOX-1")).thenReturn(java.util.List.of(10L,11L));
assertThatThrownBy(()->service.resolveWorkspaceCarton(100L,"BOX-1",7L)).isInstanceOf(BizException.class).hasMessageContaining("多个任务");
}
/** 托盘预览必须返回已封托的真实状态,跨仓不存在时拒绝。 */
@Test void workspacePalletNeverInventsOpenState(){
ReflectionTestUtils.setField(service,"workspaceRepository",workspace);
when(workspace.pallet(100L,"DP-260911-0001")).thenReturn(sealedPallet());
when(workspace.members(20L)).thenReturn(java.util.List.of());
when(mapper.cartons(20L)).thenReturn(java.util.List.of());
assertThat(service.workspacePallet(100L,"DP-260911-0001").pallet().status()).isEqualTo("SEALED");
assertThatThrownBy(()->service.workspacePallet(101L,"DP-260911-0001")).isInstanceOf(BizException.class).hasMessageContaining("未找到");
}
/** 即使不同客户允许混托,运输渠道或目的仓不同仍须拒绝。 */
@Test void incompatibleDestinationCannotBeMixed(){
when(mapper.task(10L,100L)).thenReturn(task());
when(mapper.palletCountByInbound(1L,30L)).thenReturn(2);
when(mapper.label(10L,"BOX-1")).thenReturn(label());
when(mapper.palletByNo("DP-260911-0001")).thenReturn(pallet());
when(workspace.compatible(10L,20L)).thenReturn(false);
assertThatThrownBy(()->service.countCarton(10L,new CountCartonRequest(100L,"BOX-1",java.util.List.of(new CountSkuLine(40L,"SKU-1",12,DiscrepancyAction.MATCH,null)),"DP-260911-0001","wrong-destination"),7L,"张三"))
.isInstanceOf(BizException.class).hasMessageContaining("渠道或目的仓不同");
verify(mapper,never()).addCounted(anyLong(),anyLong(),anyInt(),anyInt());
}
@Test void sharedPalletCountsDistinctSkuCodesWithoutMergingInboundDetails(){
when(mapper.inboundCode(1L,30L)).thenReturn("IN-1");
when(mapper.palletsByInbound(1L,30L)).thenReturn(java.util.List.of(pallet()));
CartonRow first=carton(),second=carton(); second.setId(61L); second.setInboundId(31L); second.setBoxCode("BOX-2");
CartonSkuRow sameSku=cartonSku(); sameSku.setInboundDetailId(41L);
CartonSkuRow differentSku=cartonSku(); differentSku.setInboundDetailId(42L); differentSku.setSkuCode("SKU-2");
when(mapper.cartons(20L)).thenReturn(java.util.List.of(first,second));
when(mapper.cartonSkus(61L)).thenReturn(java.util.List.of(sameSku));
var shared=service.inboundPallets(1L,30L).get(0);
assertThat(shared.cartonCount()).isEqualTo(2);
assertThat(shared.skuCount()).isEqualTo(1);
assertThat(shared.items()).hasSize(2);
when(mapper.cartonSkus(61L)).thenReturn(java.util.List.of(sameSku,differentSku));
assertThat(service.inboundPallets(1L,30L).get(0).skuCount()).isEqualTo(2);
when(mapper.cartons(20L)).thenReturn(java.util.List.of(first));
assertThat(service.inboundPallets(1L,30L).get(0).skuCount()).isEqualTo(1);
when(mapper.cartons(20L)).thenReturn(java.util.List.of());
assertThat(service.inboundPallets(1L,30L).get(0).skuCount()).isZero();
}
@Test void reviewCreatesExpectedNumberOfPrintablePalletCodes(){
InboundOrderEntity inbound=dropshipInbound(); when(mapper.insertTask(anyLong(),eq(1L),eq(30L),eq(100L),eq(8))).thenReturn(1);
when(numbers.next("DP")).thenReturn("DP-260911-0001","DP-260911-0002");
service.createTask(inbound,7L,"张三");
verify(mapper,times(2)).insertPallet(anyLong(),anyLong(),eq(1L),eq(30L),eq(100L),startsWith("DP-"),startsWith("dropship-precreate-"),eq(7L),eq("张三"));
}
@Test void replayedReviewDoesNotGenerateDuplicatePalletCodes(){
when(mapper.insertTask(anyLong(),anyLong(),anyLong(),anyLong(),anyInt())).thenReturn(0);
service.createTask(dropshipInbound(),7L,"张三");
verifyNoInteractions(numbers); verify(mapper,never()).insertPallet(anyLong(),anyLong(),anyLong(),anyLong(),anyLong(),anyString(),anyString(),anyLong(),anyString());
}
@Test void taskReadinessCountsGeneratedPalletsByInboundInsteadOfTaskId(){
when(mapper.task(10L,100L)).thenReturn(task());
when(mapper.palletCountByInbound(1L,30L)).thenReturn(2);
when(mapper.pallets(10L)).thenReturn(java.util.List.of());
when(mapper.unboundCartons(10L)).thenReturn(java.util.List.of());
var result=service.task(10L,100L,7L);
assertThat(result.generatedPalletCount()).isEqualTo(2);
assertThat(result.palletSetupReady()).isTrue();
verify(mapper).palletCountByInbound(1L,30L);
}
@Test void taskPrintReadsGeneratedPalletsFromTheInboundOrder(){
PalletRow generated=pallet(); generated.setTaskId(99L);
when(mapper.task(10L,100L)).thenReturn(task());
when(mapper.generatedPalletsByInbound(1L,30L)).thenReturn(java.util.List.of(generated));
when(mapper.cartons(20L)).thenReturn(java.util.List.of());
var result=service.generatedPallets(10L,100L,7L);
assertThat(result).singleElement().extracting("palletNo").isEqualTo("DP-260911-0001");
verify(mapper).generatedPalletsByInbound(1L,30L);
}
@Test void resolvesUncountedCartonBeforeQuantityConfirmation(){
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCountByInbound(1L,30L)).thenReturn(2); when(mapper.label(10L,"BOX-1")).thenReturn(label());
var result=service.resolveCarton(10L,100L,"BOX-1",7L);
assertThat(result.inboundCode()).isEqualTo("IN-1"); assertThat(result.skuLines()).extracting("expectedQuantity").containsExactly(12); assertThat(result.canCount()).isTrue();
}
@Test void cartonCanBindDirectlyToAPreGeneratedPallet(){
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCountByInbound(1L,30L)).thenReturn(2); when(mapper.label(10L,"BOX-1")).thenReturn(label()); when(mapper.palletByNo("DP-260911-0001")).thenReturn(pallet());
when(mapper.addCounted(10L,7L,1,0)).thenReturn(1); when(mapper.carton(eq(10L),anyLong())).thenReturn(carton());
when(mapper.pallet(10L,20L,100L)).thenReturn(pallet()); when(mapper.cartons(20L)).thenReturn(java.util.List.of());
service.countCarton(10L,countRequest("DP-260911-0001"),7L,"张三");
verify(mapper).insertCarton(anyLong(),eq(10L),eq(20L),eq(1L),eq(30L),eq(40L),eq(50L),eq("SKU-1"),any(),eq("BOX-1"),eq(12),eq(12),eq("MATCH"),isNull(),eq("op-1"),eq(7L),eq("张三"));
verify(mapper).insertCartonSku(anyLong(),anyLong(),eq(40L),eq("SKU-1"),any(),eq(12),eq(12),eq("MATCH"),isNull(),eq(1));
}
@Test void cartonCanBindToAnOpenPalletGeneratedForAnotherTaskInTheSameWarehouse(){
TaskRow current=task(); PalletRow shared=pallet(); shared.setTaskId(99L); shared.setTenantId(2L); shared.setInboundId(88L);
when(mapper.task(10L,100L)).thenReturn(current); when(mapper.palletCountByInbound(1L,30L)).thenReturn(2);
when(mapper.label(10L,"BOX-1")).thenReturn(label()); when(mapper.palletByNo("DP-260911-0001")).thenReturn(shared);
when(mapper.addCounted(10L,7L,1,0)).thenReturn(1); when(mapper.carton(eq(10L),anyLong())).thenReturn(carton());
when(mapper.pallet(10L,20L,100L)).thenReturn(shared); when(mapper.cartons(20L)).thenReturn(java.util.List.of());
service.countCarton(10L,countRequest("DP-260911-0001"),7L,"张三");
verify(mapper).insertCarton(anyLong(),eq(10L),eq(20L),eq(1L),eq(30L),anyLong(),anyLong(),anyString(),any(),eq("BOX-1"),anyInt(),anyInt(),eq("MATCH"),isNull(),eq("op-1"),eq(7L),eq("张三"));
}
@Test void palletFromAnotherWarehouseCannotBeMixed(){
PalletRow other=pallet(); other.setWarehouseId(101L);
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCountByInbound(1L,30L)).thenReturn(2);
when(mapper.label(10L,"BOX-1")).thenReturn(label()); when(mapper.palletByNo("DP-260911-0001")).thenReturn(other);
assertThatThrownBy(()->service.countCarton(10L,countRequest("DP-260911-0001"),7L,"张三"))
.isInstanceOf(BizException.class).hasMessageContaining("当前作业仓库");
}
@Test void unknownPalletCodeCannotCreateAnAdHocPallet(){
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCountByInbound(1L,30L)).thenReturn(2); when(mapper.label(10L,"BOX-1")).thenReturn(label());
var request=countRequest("DP-260911-9999");
assertThatThrownBy(()->service.countCarton(10L,request,7L,"张三")).isInstanceOf(BizException.class).hasMessageContaining("测量审核后生成");
verify(mapper,never()).insertPallet(anyLong(),anyLong(),anyLong(),anyLong(),anyLong(),anyString(),anyString(),anyLong(),anyString());
}
private static InboundOrderEntity dropshipInbound(){InboundOrderEntity i=new InboundOrderEntity();i.setId(30L);i.setTenantId(1L);i.setWarehouseId(100L);i.setFulfillmentType("ONE_CLICK_DROPSHIP");i.setTotalPackageCount(8);i.setPalletCount(2);return i;}
@Test void incompletePreGeneratedPalletsBlockCounting(){
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.palletCountByInbound(1L,30L)).thenReturn(0); when(mapper.label(10L,"BOX-1")).thenReturn(label());
var result=service.resolveCarton(10L,100L,"BOX-1",7L);
assertThat(result.canCount()).isFalse(); assertThat(result.blockReason()).contains("预计 2 托").contains("当前仅生成 0 个托盘码");
assertThatThrownBy(()->service.countCarton(10L,countRequest(null),7L,"张三"))
.isInstanceOf(BizException.class).hasMessageContaining("预计 2 托");
}
@Test void sealedPalletCanReopenBeforeItEntersALoadingPlan(){
TaskRow current=task(); PalletRow sealed=sealedPallet();
when(mapper.task(10L,100L)).thenReturn(current); when(mapper.pallet(10L,20L,100L)).thenReturn(sealed);
when(mapper.taskIdsByPallet(20L)).thenReturn(java.util.List.of(10L)); when(mapper.unseal(10L,20L,1,100L,"undo-1")).thenReturn(1);
when(mapper.palletCountByInbound(1L,30L)).thenReturn(2); when(mapper.pallets(10L)).thenReturn(java.util.List.of());
var result=service.unseal(10L,20L,new UnsealPalletRequest(100L,"undo-1",1),7L,"张三");
assertThat(result.taskStatus()).isEqualTo("CLAIMED");
verify(mapper).activeLoadingAssignmentCount(20L); verify(mapper).unseal(10L,20L,1,100L,"undo-1");
verify(mapper,never()).reopenTask(anyLong()); verify(mapper,never()).reopenInbound(anyLong(),anyLong(),anyLong());
}
@Test void unsealingACompletedSharedPalletReopensEveryParticipatingInbound(){
TaskRow completed=task(); completed.setTaskStatus("COMPLETED"); completed.setProcessStatus("WAIT_SPLIT");
TaskRow other=task(); other.setId(11L); other.setTenantId(2L); other.setInboundId(31L); other.setTaskStatus("COMPLETED"); other.setProcessStatus("WAIT_SPLIT");
TaskRow reopened=task();
when(mapper.task(10L,100L)).thenReturn(completed,completed,reopened);
when(mapper.task(11L,100L)).thenReturn(other); when(mapper.pallet(10L,20L,100L)).thenReturn(sealedPallet());
when(mapper.taskIdsByPallet(20L)).thenReturn(java.util.List.of(10L,11L)); when(mapper.unseal(10L,20L,1,100L,"undo-2")).thenReturn(1);
when(mapper.reopenTask(10L)).thenReturn(1); when(mapper.reopenTask(11L)).thenReturn(1);
when(mapper.reopenInbound(1L,30L,7L)).thenReturn(1); when(mapper.reopenInbound(2L,31L,7L)).thenReturn(1);
when(mapper.palletCountByInbound(1L,30L)).thenReturn(2); when(mapper.pallets(10L)).thenReturn(java.util.List.of());
service.unseal(10L,20L,new UnsealPalletRequest(100L,"undo-2",1),7L,"张三");
verify(mapper).reopenTask(10L); verify(mapper).reopenTask(11L);
verify(mapper).reopenInbound(1L,30L,7L); verify(mapper).reopenInbound(2L,31L,7L);
}
@Test void palletAssignedToALoadingPlanCannotBeUnsealed(){
when(mapper.task(10L,100L)).thenReturn(task()); when(mapper.pallet(10L,20L,100L)).thenReturn(sealedPallet());
when(mapper.activeLoadingAssignmentCount(20L)).thenReturn(1);
assertThatThrownBy(()->service.unseal(10L,20L,new UnsealPalletRequest(100L,"undo-3",1),7L,"张三"))
.isInstanceOf(BizException.class).hasMessageContaining("已进入装柜计划");
verify(mapper,never()).unseal(anyLong(),anyLong(),anyInt(),anyLong(),anyString());
}
private static TaskRow task(){TaskRow t=new TaskRow();t.setId(10L);t.setTenantId(1L);t.setInboundId(30L);t.setWarehouseId(100L);t.setInboundCode("IN-1");t.setTaskStatus("CLAIMED");t.setAssigneeId(7L);t.setProcessStatus("WAIT_PALLETIZE");t.setTotalCartons(2);t.setCountedCartons(0);t.setAllocatedCartons(0);t.setDiscrepancyCartons(0);t.setExpectedPalletCount(2);t.setVersion(1);return t;}
private static LabelRow label(){LabelRow l=new LabelRow();l.setCartonLabelId(50L);l.setInboundDetailId(40L);l.setSkuCode("SKU-1");l.setCargoName("货物");l.setUnitsPerCarton(12);l.setInboundCode("IN-1");l.setBoxCode("BOX-1");return l;}
private static LabelSkuRow labelSku(){LabelSkuRow l=new LabelSkuRow();l.setInboundDetailId(40L);l.setSkuCode("SKU-1");l.setCargoName("货物");l.setExpectedQuantity(12);l.setLineNo(1);return l;}
private static CartonSkuRow cartonSku(){CartonSkuRow l=new CartonSkuRow();l.setInboundDetailId(40L);l.setSkuCode("SKU-1");l.setCargoName("货物");l.setExpectedQuantity(12);l.setActualQuantity(12);l.setDiscrepancyStatus("MATCH");l.setLineNo(1);return l;}
private static CountCartonRequest countRequest(String palletNo){return new CountCartonRequest(100L,"BOX-1",java.util.List.of(new CountSkuLine(40L,"SKU-1",12,DiscrepancyAction.MATCH,null)),palletNo,"op-1");}
private static PalletRow pallet(){PalletRow p=new PalletRow();p.setId(20L);p.setTaskId(10L);p.setTenantId(1L);p.setInboundId(30L);p.setWarehouseId(100L);p.setPalletNo("DP-260911-0001");p.setPalletStatus("OPEN");p.setVersion(1);return p;}
private static PalletRow sealedPallet(){PalletRow p=pallet();p.setPalletStatus("SEALED");return p;}
private static CartonRow carton(){CartonRow c=new CartonRow();c.setId(60L);c.setTaskId(10L);c.setPalletId(20L);c.setInboundId(30L);c.setInboundDetailId(40L);c.setPalletNo("DP-260911-0001");c.setSkuCode("SKU-1");c.setCargoName("货物");c.setBoxCode("BOX-1");c.setExpectedSkuQuantity(12);c.setActualSkuQuantity(12);c.setDiscrepancyStatus("MATCH");return c;}
}