暂存提交
This commit is contained in:
+374
@@ -0,0 +1,374 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.service;
|
||||
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
import com.baiyue.lastleg.inboundplan.pda.dto.PdaRequests;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPalletMerge;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaOperation;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.inboundplan.pda.repository.PdaWorkflowRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundPdaWorkflowServiceTest {
|
||||
@Mock PdaWorkflowRepository repository;
|
||||
@Mock WarehouseResourceGateway warehouse;
|
||||
@Mock InboundStockReceiptService stockReceipts;
|
||||
private InboundPdaWorkflowService service;
|
||||
|
||||
private InboundPlan plan;
|
||||
private InboundPdaTask task;
|
||||
private InboundPlanLine targetLine;
|
||||
private InboundPlanLine sourceLine;
|
||||
private InboundPlanPallet targetPallet;
|
||||
private InboundPlanPallet sourcePallet;
|
||||
private InboundPlanPallet otherWarehousePallet;
|
||||
private InboundPlanPalletAllocation target;
|
||||
private InboundPlanPalletAllocation source;
|
||||
private InboundPlanPalletAllocation otherWarehouse;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InboundPdaWorkflowService();
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
ReflectionTestUtils.setField(service, "warehouse", warehouse);
|
||||
ReflectionTestUtils.setField(service, "stockReceipts", stockReceipts);
|
||||
ReflectionTestUtils.setField(service, "sourceObjectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
service.initializeObjectMapper();
|
||||
plan = plan();
|
||||
task = task();
|
||||
targetLine = line(11L, 101L, "RK-A", "1.0000", 5);
|
||||
sourceLine = line(12L, 102L, "RK-B", "0.5000", 2);
|
||||
targetPallet = pallet(21L, "WH-A", 3, "0.6000", 901L);
|
||||
sourcePallet = pallet(22L, "WH-A", 2, "0.5000", 902L);
|
||||
otherWarehousePallet = pallet(23L, "WH-B", 4, "0.8000", 903L);
|
||||
target = allocation(31L, targetLine.getId(), targetPallet.getId(), 3, 3, 0);
|
||||
source = allocation(32L, sourceLine.getId(), sourcePallet.getId(), 2, 2, 2);
|
||||
otherWarehouse = allocation(33L, sourceLine.getId(), otherWarehousePallet.getId(), 2, 2, 2);
|
||||
when(repository.lockPlan("100", "LIP1")).thenReturn(plan);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushRequiresEveryPalletToBeAssignedAndLocked() {
|
||||
targetPallet.setWarehouseResourceLockId(null);
|
||||
when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet));
|
||||
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> service.push("100", "LIP1", new PdaRequests.Push("push-1", "7", "worker")));
|
||||
|
||||
verify(repository, never()).insert(any(InboundPdaTask.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assignmentReservesWarehouseResourcesAndPersistsTheLock() {
|
||||
targetPallet.setDestinationWarehouseId("88");
|
||||
targetPallet.setWarehousePalletId(null);
|
||||
targetPallet.setWarehouseLocationId(null);
|
||||
targetPallet.setWarehouseResourceLockId(null);
|
||||
targetPallet.setAssignmentStatus("PENDING_ASSIGNMENT");
|
||||
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
|
||||
when(warehouse.reserve("100", 88L, 188L, 288L, "INBOUND_PLAN_PALLET", "LIP1:21", "assign-1"))
|
||||
.thenReturn(999L);
|
||||
when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet));
|
||||
|
||||
service.assign("100", "LIP1", targetPallet.getId(),
|
||||
new PdaRequests.Assign(88L, 188L, 288L, "assign-1"));
|
||||
|
||||
assertEquals(188L, targetPallet.getWarehousePalletId());
|
||||
assertEquals(288L, targetPallet.getWarehouseLocationId());
|
||||
assertEquals(999L, targetPallet.getWarehouseResourceLockId());
|
||||
assertEquals("ASSIGNED", targetPallet.getAssignmentStatus());
|
||||
assertEquals("ASSIGNED", plan.getStatus());
|
||||
verify(repository).update(targetPallet);
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedAssignmentIsIdempotentAndWrongDestinationIsRejected() {
|
||||
targetPallet.setDestinationWarehouseId("88");
|
||||
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
|
||||
|
||||
service.assign("100", "LIP1", targetPallet.getId(),
|
||||
new PdaRequests.Assign(88L, targetPallet.getWarehousePalletId(),
|
||||
targetPallet.getWarehouseLocationId(), "assign-retry"));
|
||||
|
||||
verify(warehouse, never()).reserve(any(), anyLong(), anyLong(), anyLong(),
|
||||
any(), any(), any());
|
||||
assertThrows(ResponseStatusException.class, () -> service.assign("100", "LIP1", targetPallet.getId(),
|
||||
new PdaRequests.Assign(99L, targetPallet.getWarehousePalletId(),
|
||||
targetPallet.getWarehouseLocationId(), "assign-wrong-warehouse")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void countMustPrecedePickAndPickedBoxesDriveActualVolume() {
|
||||
activeTask();
|
||||
target.setCountedPackageCount(0);
|
||||
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
|
||||
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
|
||||
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.pick("100", "LIP1", target.getId(),
|
||||
new PdaRequests.PackageOperation(1, "pick-before-count", "7", "worker")));
|
||||
|
||||
service.count("100", "LIP1", target.getId(),
|
||||
new PdaRequests.PackageOperation(3, "count-3", "7", "worker"));
|
||||
var result = service.pick("100", "LIP1", target.getId(),
|
||||
new PdaRequests.PackageOperation(2, "pick-2", "7", "worker"));
|
||||
|
||||
assertEquals("PICKED", result.result());
|
||||
assertEquals(2, target.getPickedPackageCount());
|
||||
assertEquals("0.4000", target.getActualVolumeCbm().toPlainString());
|
||||
assertEquals(5, targetPallet.getActualPackageCount());
|
||||
assertEquals("1.0000", targetPallet.getActualVolumeCbm().toPlainString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void overPickReturnsConfirmationWithoutChangingAllocationOrPallet() {
|
||||
activeTask();
|
||||
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
|
||||
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
|
||||
candidateQueries();
|
||||
|
||||
var result = service.pick("100", "LIP1", target.getId(),
|
||||
new PdaRequests.PackageOperation(5, "pick-over", "7", "worker"));
|
||||
|
||||
assertEquals("MERGE_CONFIRMATION_REQUIRED", result.result());
|
||||
assertEquals(3, result.remainingPackageCount());
|
||||
assertEquals(2, result.mergePackageCount());
|
||||
assertEquals(1, result.candidates().size());
|
||||
assertEquals(source.getId(), result.candidates().get(0).allocationId());
|
||||
assertEquals(0, target.getPickedPackageCount());
|
||||
verify(repository, never()).update(target);
|
||||
verify(repository, never()).update(targetPallet);
|
||||
verify(repository).insert(any(InboundPdaOperation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeCandidatesOnlyIncludeSameDestinationPallets() {
|
||||
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
|
||||
candidateQueries();
|
||||
|
||||
var result = service.mergeCandidates("100", "LIP1", target.getId());
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("RK-B", result.get(0).inboundCode());
|
||||
assertEquals("0.5000", result.get(0).availableVolumeCbm().toPlainString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmedMergePersistsBothOrdersAndScrapsAnEmptiedSourcePallet() {
|
||||
activeTask();
|
||||
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
|
||||
when(repository.lockAllocation(plan.getId(), source.getId())).thenReturn(source);
|
||||
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
|
||||
when(repository.lockPallet(plan.getId(), sourcePallet.getId())).thenReturn(sourcePallet);
|
||||
when(repository.line(plan.getId(), sourceLine.getId())).thenReturn(sourceLine);
|
||||
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
|
||||
when(repository.allocations(plan.getId())).thenReturn(List.of(target, source));
|
||||
|
||||
var result = service.merge("100", "LIP1", target.getId(),
|
||||
new PdaRequests.ConfirmMerge(source.getId(), 2, "merge-1", "7", "worker"));
|
||||
|
||||
assertEquals("MERGED", result.result());
|
||||
assertEquals(sourceLine.getOrderId(), result.sourceOrderId());
|
||||
assertEquals(targetLine.getOrderId(), result.targetOrderId());
|
||||
assertTrue(result.sourcePalletScrapped());
|
||||
assertEquals(0, sourcePallet.getActualPackageCount());
|
||||
assertEquals(5, targetPallet.getActualPackageCount());
|
||||
verify(warehouse).scrap("100", 902L);
|
||||
ArgumentCaptor<InboundPalletMerge> merge = ArgumentCaptor.forClass(InboundPalletMerge.class);
|
||||
verify(repository).insert(merge.capture());
|
||||
assertEquals(sourceLine.getOrderId(), merge.getValue().getSourceOrderId());
|
||||
assertEquals(targetLine.getOrderId(), merge.getValue().getTargetOrderId());
|
||||
assertTrue(merge.getValue().getSourcePalletScrapped());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeCannotExceedTargetPalletVolumeCapacity() {
|
||||
activeTask();
|
||||
targetPallet.setActualVolumeCbm(new BigDecimal("1.9000"));
|
||||
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
|
||||
when(repository.lockAllocation(plan.getId(), source.getId())).thenReturn(source);
|
||||
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
|
||||
when(repository.lockPallet(plan.getId(), sourcePallet.getId())).thenReturn(sourcePallet);
|
||||
when(repository.line(plan.getId(), sourceLine.getId())).thenReturn(sourceLine);
|
||||
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.merge("100", "LIP1", target.getId(),
|
||||
new PdaRequests.ConfirmMerge(source.getId(), 1, "merge-over-capacity", "7", "worker")));
|
||||
|
||||
verify(warehouse, never()).scrap(any(), anyLong());
|
||||
verify(repository, never()).insert(any(InboundPalletMerge.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completionRequiresExactPickedTotalForEveryLine() {
|
||||
activeTask();
|
||||
when(repository.allocations(plan.getId())).thenReturn(List.of(target));
|
||||
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.complete("100", "LIP1"));
|
||||
assertFalse("COMPLETED".equals(task.getStatus()));
|
||||
verify(repository, never()).update(plan);
|
||||
}
|
||||
|
||||
@Test
|
||||
void completesWhenMergedAllocationsPreserveExactLineTotals() {
|
||||
activeTask();
|
||||
target.setPickedPackageCount(3);
|
||||
InboundPlanPalletAllocation moved = allocation(34L, targetLine.getId(), sourcePallet.getId(), 0, 2, 2);
|
||||
when(repository.allocations(plan.getId())).thenReturn(List.of(target, moved));
|
||||
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
|
||||
|
||||
var result = service.complete("100", "LIP1");
|
||||
|
||||
assertEquals("COMPLETED", result.status());
|
||||
assertEquals("COMPLETED", plan.getStatus());
|
||||
verify(repository).update(task);
|
||||
verify(repository).update(plan);
|
||||
verify(stockReceipts).stockIn(eq("100"), eq(plan), eq(task), eq(List.of(targetLine)),
|
||||
eq(List.of(target, moved)), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedTaskReplayDoesNotRunStockInAgain() {
|
||||
task.setStatus("COMPLETED");
|
||||
activeTask();
|
||||
|
||||
var result = service.complete("100", "LIP1");
|
||||
|
||||
assertEquals("COMPLETED", result.status());
|
||||
verify(stockReceipts, never()).stockIn(any(), any(), any(), any(), any(), any(), any());
|
||||
verify(repository, never()).allocations(plan.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReuseOfIdempotencyKeyWithDifferentActionOrPayload() {
|
||||
InboundPdaOperation existing = new InboundPdaOperation();
|
||||
existing.setAction("COUNT");
|
||||
existing.setRequestHash("different");
|
||||
when(repository.operation("100", "same-key")).thenReturn(existing);
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.pick("100", "LIP1", target.getId(),
|
||||
new PdaRequests.PackageOperation(1, "same-key", "7", "worker")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void completedTaskStillReplaysAnAlreadyPersistedPick() {
|
||||
activeTask();
|
||||
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
|
||||
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
|
||||
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
|
||||
PdaRequests.PackageOperation request = new PdaRequests.PackageOperation(1, "pick-replay", "7", "worker");
|
||||
|
||||
var first = service.pick("100", "LIP1", target.getId(), request);
|
||||
ArgumentCaptor<InboundPdaOperation> operation = ArgumentCaptor.forClass(InboundPdaOperation.class);
|
||||
verify(repository).insert(operation.capture());
|
||||
task.setStatus("COMPLETED");
|
||||
when(repository.operation("100", "pick-replay")).thenReturn(operation.getValue());
|
||||
|
||||
var replay = service.pick("100", "LIP1", target.getId(), request);
|
||||
|
||||
assertEquals(first, replay);
|
||||
assertEquals(1, target.getPickedPackageCount());
|
||||
verify(repository, times(1)).lockAllocation(plan.getId(), target.getId());
|
||||
}
|
||||
|
||||
private void activeTask() { when(repository.lockTask("100", plan.getId())).thenReturn(task); }
|
||||
|
||||
private void candidateQueries() {
|
||||
when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet, sourcePallet, otherWarehousePallet));
|
||||
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine, sourceLine));
|
||||
when(repository.allocations(plan.getId())).thenReturn(List.of(target, source, otherWarehouse));
|
||||
}
|
||||
|
||||
private InboundPlan plan() {
|
||||
InboundPlan value = new InboundPlan();
|
||||
value.setId(1L);
|
||||
value.setCustomerId("100");
|
||||
value.setPlanNo("LIP1");
|
||||
value.setStatus("ASSIGNED");
|
||||
return value;
|
||||
}
|
||||
|
||||
private InboundPdaTask task() {
|
||||
InboundPdaTask value = new InboundPdaTask();
|
||||
value.setId(2L);
|
||||
value.setPlanId(1L);
|
||||
value.setCustomerId("100");
|
||||
value.setTaskNo("PDA2");
|
||||
value.setStatus("PDA_PENDING");
|
||||
value.setVersion(0);
|
||||
return value;
|
||||
}
|
||||
|
||||
private InboundPlanLine line(long id, long orderId, String inboundCode, String volume, int packages) {
|
||||
InboundPlanLine value = new InboundPlanLine();
|
||||
value.setId(id);
|
||||
value.setPlanId(1L);
|
||||
value.setOrderId(orderId);
|
||||
value.setInboundCode(inboundCode);
|
||||
value.setDestinationWarehouseId("WH-A");
|
||||
value.setVolumeCbm(new BigDecimal(volume));
|
||||
value.setRequiredPackageCount(packages);
|
||||
return value;
|
||||
}
|
||||
|
||||
private InboundPlanPallet pallet(long id, String warehouseId, int actualPackages,
|
||||
String actualVolume, long lockId) {
|
||||
InboundPlanPallet value = new InboundPlanPallet();
|
||||
value.setId(id);
|
||||
value.setPlanId(1L);
|
||||
value.setPalletNo("PLT-" + id);
|
||||
value.setDestinationWarehouseId(warehouseId);
|
||||
value.setCapacityCbm(new BigDecimal("2.0000"));
|
||||
value.setWarehousePalletId(id + 1000);
|
||||
value.setWarehouseLocationId(id + 2000);
|
||||
value.setWarehouseResourceLockId(lockId);
|
||||
value.setAssignmentStatus("ASSIGNED");
|
||||
value.setActualPackageCount(actualPackages);
|
||||
value.setActualVolumeCbm(new BigDecimal(actualVolume));
|
||||
return value;
|
||||
}
|
||||
|
||||
private InboundPlanPalletAllocation allocation(long id, long lineId, long palletId,
|
||||
int planned, int counted, int picked) {
|
||||
InboundPlanPalletAllocation value = new InboundPlanPalletAllocation();
|
||||
value.setId(id);
|
||||
value.setPlanId(1L);
|
||||
value.setLineId(lineId);
|
||||
value.setPalletId(palletId);
|
||||
value.setPlannedPackageCount(planned);
|
||||
value.setCountedPackageCount(counted);
|
||||
value.setPickedPackageCount(picked);
|
||||
value.setActualVolumeCbm(BigDecimal.ZERO);
|
||||
value.setVersion(0);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.service;
|
||||
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundStockReceipt;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundStockReceiptMapper;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundStockReceiptServiceTest {
|
||||
@Mock LastlegOrderMapper orders;
|
||||
@Mock InboundStockReceiptMapper receipts;
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock DropshipOrderService dropshipOrders;
|
||||
@Mock LastlegOrderStatusHistoryMapper orderHistories;
|
||||
private InboundStockReceiptService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InboundStockReceiptService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "receipts", receipts);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
|
||||
ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
|
||||
when(receipts.listTask("100", 2L)).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void writesOneReceiptPerSkuAndPalletAndUsesPdaPackageCount() {
|
||||
Fixture fixture = fixture(5, 5, 5, 10);
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
|
||||
|
||||
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
|
||||
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now());
|
||||
|
||||
ArgumentCaptor<InventoryCommandRequests.StockIn> request = ArgumentCaptor.forClass(InventoryCommandRequests.StockIn.class);
|
||||
verify(inventory).stockIn(eq("100"), eq("operator"), request.capture());
|
||||
assertEquals(5L, request.getValue().packages());
|
||||
assertEquals(101L, request.getValue().inboundOrderId());
|
||||
assertEquals("IN-1", request.getValue().inboundCode());
|
||||
ArgumentCaptor<InboundStockReceipt> receipt = ArgumentCaptor.forClass(InboundStockReceipt.class);
|
||||
verify(receipts).insert(receipt.capture());
|
||||
assertEquals(5L, receipt.getValue().getReceivedPackages());
|
||||
assertEquals(10L, receipt.getValue().getInventoryPositionId());
|
||||
assertEquals("INBOUND_COMPLETED", fixture.order.getCurrentStatus());
|
||||
assertEquals("INBOUND_PLAN:COMPLETED", fixture.order.getSubOrderStatus());
|
||||
verify(orderHistories).insert(any(LastlegOrderStatusHistory.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void storesOneInboundOrderAcrossMultiplePalletsWithoutSkuSplitting() {
|
||||
Fixture fixture = fixture(10, 5, 5, 10);
|
||||
fixture.detail.setPackageCount(5);
|
||||
LastlegOrderDetail second = detail(202L, "SKU-2", 5, 7);
|
||||
InboundPlanPallet secondPallet = pallet(22L, 2002L);
|
||||
InboundPlanPalletAllocation secondAllocation = allocation(32L, 12L, 22L, 5);
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
|
||||
|
||||
service.stockIn("100", fixture.plan, fixture.task,
|
||||
List.of(fixture.line), List.of(fixture.allocation, secondAllocation),
|
||||
List.of(fixture.pallet, secondPallet), LocalDateTime.now());
|
||||
verify(inventory, times(2)).stockIn(eq("100"), eq("operator"), any());
|
||||
verify(receipts, times(2)).insert(any(InboundStockReceipt.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void conservesSingleSkuPackagesAndQuantityAcrossPallets() {
|
||||
Fixture fixture = fixture(5, 2, 2, 11);
|
||||
InboundPlanPallet secondPallet = pallet(22L, 2002L);
|
||||
InboundPlanPalletAllocation secondAllocation = allocation(32L, 12L, 22L, 3);
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
|
||||
|
||||
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
|
||||
List.of(fixture.allocation, secondAllocation), List.of(fixture.pallet, secondPallet), LocalDateTime.now());
|
||||
|
||||
ArgumentCaptor<InventoryCommandRequests.StockIn> requests = ArgumentCaptor.forClass(InventoryCommandRequests.StockIn.class);
|
||||
verify(inventory, times(2)).stockIn(eq("100"), eq("operator"), requests.capture());
|
||||
assertEquals(5L, requests.getAllValues().stream().mapToLong(InventoryCommandRequests.StockIn::packages).sum());
|
||||
}
|
||||
|
||||
@Test
|
||||
void existingTaskReceiptMakesStockInReplayANoOp() {
|
||||
Fixture fixture = fixture(10, 5, 5, 10);
|
||||
InboundPlanPallet secondPallet = pallet(22L, 2002L);
|
||||
InboundPlanPalletAllocation secondAllocation = allocation(32L, 12L, 22L, 5);
|
||||
InboundStockReceipt firstReceipt = new InboundStockReceipt();
|
||||
firstReceipt.setAllocationId(fixture.allocation.getId()); firstReceipt.setOrderId(fixture.order.getId());
|
||||
InboundStockReceipt secondReceipt = new InboundStockReceipt();
|
||||
secondReceipt.setAllocationId(secondAllocation.getId()); secondReceipt.setOrderId(fixture.order.getId());
|
||||
when(receipts.listTask("100", fixture.task.getId())).thenReturn(List.of(firstReceipt, secondReceipt));
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
|
||||
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
|
||||
List.of(fixture.allocation, secondAllocation), List.of(fixture.pallet, secondPallet), LocalDateTime.now());
|
||||
|
||||
verify(inventory, never()).stockIn(any(), any(), any());
|
||||
verify(receipts, never()).insert(any(InboundStockReceipt.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialTaskReceiptsAreRejected() {
|
||||
Fixture fixture = fixture(10, 5, 5, 10);
|
||||
InboundPlanPallet secondPallet = pallet(22L, 2002L);
|
||||
InboundPlanPalletAllocation secondAllocation = allocation(32L, 12L, 22L, 5);
|
||||
InboundStockReceipt partial = new InboundStockReceipt();
|
||||
partial.setAllocationId(fixture.allocation.getId()); partial.setOrderId(fixture.order.getId());
|
||||
when(receipts.listTask("100", fixture.task.getId())).thenReturn(List.of(partial));
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.stockIn("100", fixture.plan, fixture.task,
|
||||
List.of(fixture.line), List.of(fixture.allocation, secondAllocation),
|
||||
List.of(fixture.pallet, secondPallet), LocalDateTime.now()));
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
|
||||
verify(inventory, never()).stockIn(any(), any(), any());
|
||||
verify(dropshipOrders, never()).activateAfterStockIn(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void activatesDropshipAfterGenericStockInReceiptIsCreated() {
|
||||
Fixture fixture = fixture(5, 5, 5, 10);
|
||||
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
|
||||
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
|
||||
|
||||
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
|
||||
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now());
|
||||
|
||||
ArgumentCaptor<List<InboundStockReceipt>> activationReceipts = ArgumentCaptor.forClass(List.class);
|
||||
verify(dropshipOrders).activateAfterStockIn(eq("100"), eq(101L), eq(2L), activationReceipts.capture());
|
||||
assertEquals(1, activationReceipts.getValue().size());
|
||||
assertEquals(10L, activationReceipts.getValue().get(0).getInventoryPositionId());
|
||||
}
|
||||
|
||||
private Fixture fixture(int required, int planned, int picked, int quantity) {
|
||||
InboundPlan plan = new InboundPlan(); plan.setId(1L); plan.setPlanNo("IP-1");
|
||||
InboundPdaTask task = new InboundPdaTask(); task.setId(2L); task.setOperatorId("operator");
|
||||
InboundPlanLine line = new InboundPlanLine(); line.setId(12L); line.setPlanId(1L); line.setOrderId(101L);
|
||||
line.setInboundCode("IN-1"); line.setDestinationWarehouseId("WH-DEST");
|
||||
line.setDestinationWarehouseCode("ONT8"); line.setDestinationWarehouseName("ONT8");
|
||||
line.setRequiredPackageCount(required); line.setVolumeCbm(new BigDecimal("1.0"));
|
||||
LastlegOrder order = new LastlegOrder(); order.setId(101L); order.setCustomerId("100");
|
||||
order.setOrderNo("LMO-1"); order.setCustomerName("Customer"); order.setOverseasWarehouseId("OW-1");
|
||||
order.setOverseasWarehouseName("Warehouse"); order.setSourceEventId("evt-1"); order.setFbaWarehouseCode("ONT8");
|
||||
order.setCurrentStatus("INBOUND_PLANNED");
|
||||
org.mockito.Mockito.lenient().when(orders.lockById("100", 101L)).thenReturn(order);
|
||||
LastlegOrderDetail detail = detail(201L, "SKU-1", required, quantity);
|
||||
return new Fixture(plan, task, line, order, detail, allocation(31L, 12L, 21L, picked), pallet(21L, 2001L));
|
||||
}
|
||||
|
||||
private LastlegOrderDetail detail(long id, String sku, int packages, int quantity) {
|
||||
LastlegOrderDetail detail = new LastlegOrderDetail(); detail.setId(id); detail.setOrderId(101L);
|
||||
detail.setSkuCode(sku); detail.setPackageCount(packages); detail.setProductQuantity(quantity);
|
||||
detail.setProductNameZh(sku + " name"); return detail;
|
||||
}
|
||||
|
||||
private InboundPlanPallet pallet(long id, long warehousePalletId) {
|
||||
InboundPlanPallet pallet = new InboundPlanPallet(); pallet.setId(id); pallet.setPlanId(1L);
|
||||
pallet.setPalletNo("PLT-" + id); pallet.setWarehousePalletId(warehousePalletId);
|
||||
pallet.setWarehouseLocationId(3001L); return pallet;
|
||||
}
|
||||
|
||||
private InboundPlanPalletAllocation allocation(long id, long lineId, long palletId, int picked) {
|
||||
InboundPlanPalletAllocation allocation = new InboundPlanPalletAllocation(); allocation.setId(id);
|
||||
allocation.setPlanId(1L); allocation.setLineId(lineId); allocation.setPalletId(palletId);
|
||||
allocation.setPlannedPackageCount(picked); allocation.setPickedPackageCount(picked); return allocation;
|
||||
}
|
||||
|
||||
private record Fixture(InboundPlan plan, InboundPdaTask task, InboundPlanLine line, LastlegOrder order,
|
||||
LastlegOrderDetail detail, InboundPlanPalletAllocation allocation,
|
||||
InboundPlanPallet pallet) {}
|
||||
}
|
||||
Reference in New Issue
Block a user