feat: complete overseas pda warehouse operations

This commit is contained in:
2026-08-25 11:20:36 +08:00
parent 45a3b9adfd
commit 042d0b738a
69 changed files with 4679 additions and 4 deletions
@@ -58,4 +58,5 @@ class HttpWarehouseResourceGatewayTest {
assertThat(result.palletId()).isEqualTo(2092040891261427713L);
server.verify();
}
}
@@ -0,0 +1,27 @@
package com.baiyue.lastleg.palletops.controller;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
class PdaPalletOperationControllerContractTest {
@Test void exposesOnlyDedicatedPdaPalletEndpointsWithCanonicalPickerAuthority() {
RequestMapping mapping = PdaPalletOperationController.class.getAnnotation(RequestMapping.class);
assertArrayEquals(new String[]{"/api/pda/v1/last-leg/pallet-operations"}, mapping.value());
PreAuthorize authorize = PdaPalletOperationController.class.getAnnotation(PreAuthorize.class);
assertEquals("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')", authorize.value());
Set<String> paths = Arrays.stream(PdaPalletOperationController.class.getDeclaredMethods())
.map(Method::getDeclaredAnnotations).flatMap(Arrays::stream)
.filter(PostMapping.class::isInstance).map(PostMapping.class::cast)
.flatMap(annotation -> Arrays.stream(annotation.value())).collect(Collectors.toSet());
assertEquals(Set.of("/pallets/resolve", "/assemble", "/disassemble", "/swap", "/unbind"), paths);
}
}
@@ -0,0 +1,124 @@
package com.baiyue.lastleg.palletops.service;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryLedgerMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.palletops.dto.PalletOperationRequests;
import com.baiyue.lastleg.palletops.entity.PdaPalletOperation;
import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway;
import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper;
import com.baiyue.lastleg.palletops.model.PalletInventoryRow;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
import com.baiyue.lastleg.pda.warehouse.service.PdaWarehouseService;
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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class PdaPalletOperationServiceTest {
@Mock private PdaPalletOperationMapper mapper;
@Mock private InventoryPositionMapper positions;
@Mock private InventoryBalanceMapper balances;
@Mock private InventoryLedgerMapper ledgers;
@Mock private PalletPhysicalGateway physical;
@Mock private PdaWarehouseService history;
private PdaPalletOperationService service;
@BeforeEach void setUp() {
service = new PdaPalletOperationService();
ReflectionTestUtils.setField(service, "mapper", mapper);
ReflectionTestUtils.setField(service, "positions", positions);
ReflectionTestUtils.setField(service, "balances", balances);
ReflectionTestUtils.setField(service, "ledgers", ledgers);
ReflectionTestUtils.setField(service, "physical", physical);
ReflectionTestUtils.setField(service, "history", history);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
service.configureTransactions(new NoOpTransactionManager());
}
@Test void resolveRejectsPalletOutsideTokenFrozenWarehouse() {
when(physical.resolve("P-1")).thenReturn(pallet(10L, "P-1", 99L, 1));
assertThrows(ResponseStatusException.class, () -> service.resolve(identity(),
new PalletOperationRequests.Scan("P-1")));
verifyNoInteractions(mapper);
}
@Test void localConcurrencyFailureCompensatesEveryAppliedPhysicalTransitionInReverseSafeForm() {
var source = pallet(10L, "SOURCE", 88L, 1);
var target = pallet(20L, "TARGET", 88L, 2);
when(physical.resolve("SOURCE")).thenReturn(source);
when(physical.resolve("TARGET")).thenReturn(target);
when(physical.resolveLocation(88L, target.locationId())).thenReturn(
new PalletPhysicalGateway.LocationReference(target.locationId(), "LOC-" + target.locationId(),
"STORAGE", "ACTIVE"));
PalletInventoryRow planned = row(100L, 10L, 1L, 5L);
PalletInventoryRow stale = row(100L, 10L, 2L, 5L);
when(mapper.content("88", 10L)).thenReturn(List.of(planned));
when(mapper.content("88", 20L)).thenReturn(List.of());
when(mapper.palletPackages("88", 10L)).thenReturn(5L);
when(mapper.palletPackages("88", 20L)).thenReturn(0L);
when(mapper.lockPosition("88", 100L)).thenReturn(stale);
AtomicReference<PdaPalletOperation> operation = new AtomicReference<>();
doAnswer(invocation -> { operation.set(invocation.getArgument(0)); return 1; })
.when(mapper).insert(any(PdaPalletOperation.class));
when(mapper.selectById(anyLong())).thenAnswer(invocation -> operation.get());
when(physical.apply(eq(88L), any(), anyString(), anyLong(), anyString(), any()))
.thenAnswer(invocation -> transition(invocation.getArgument(1), invocation.getArgument(2)));
var request = new PalletOperationRequests.Assemble("idem-1",
new PalletOperationRequests.PalletExpectation("TARGET", 2),
List.of(new PalletOperationRequests.PalletExpectation("SOURCE", 1)),
List.of(new PalletOperationRequests.MoveLine(100L, 5L, 1L)), "assemble");
assertThrows(ResponseStatusException.class, () -> service.assemble(identity(), request));
verify(physical, times(2)).compensate(eq(88L), any(PalletPhysicalGateway.Transition.class));
assertEquals("FAILED_COMPENSATED", operation.get().getStatus());
verify(positions, never()).updateById(any(com.baiyue.lastleg.inventory.entity.InventoryPosition.class));
verifyNoInteractions(ledgers, history);
}
private static PdaWarehouseIdentity identity() { return new PdaWarehouseIdentity("7", "picker", 88L); }
private static PalletPhysicalGateway.PalletReference pallet(long id, String code, long warehouse, int version) {
return new PalletPhysicalGateway.PalletReference(id, code, warehouse, 1000L + id,
id == 20L ? "IDLE" : "OCCUPIED", null, version);
}
private static PalletPhysicalGateway.Transition transition(PalletPhysicalGateway.PalletReference pallet,
String targetStatus) {
return new PalletPhysicalGateway.Transition("op-" + pallet.palletId(), pallet.palletId(), pallet.palletCode(),
pallet.warehouseId(), pallet.status(), targetStatus, pallet.locationId(), pallet.locationId(),
pallet.version(), pallet.version() + 1, "APPLIED", false);
}
private static PalletInventoryRow row(long positionId, long palletId, long version, long packages) {
PalletInventoryRow value = new PalletInventoryRow(); value.setCustomerId("100"); value.setCustomerName("C");
value.setBalanceId(200L); value.setPositionId(positionId); value.setInboundCode("IN-1");
value.setSkuCode("SKU-1"); value.setDestinationWarehouseCode("ONT8"); value.setPalletId(palletId);
value.setPalletNo(palletId == 10L ? "SOURCE" : "TARGET"); value.setBatchNo("DEFAULT");
value.setWarehouseLocationId(1010L); value.setWarehouseLocationCode("L-1");
value.setAvailablePackages(packages); value.setHoldPackages(0L); value.setAllocatedPackages(0L);
value.setPickedPackages(0L); value.setReceivingPackages(0L); value.setVersion(version); return value;
}
private static final class NoOpTransactionManager extends AbstractPlatformTransactionManager {
@Override protected Object doGetTransaction() { return new Object(); }
@Override protected void doBegin(Object transaction, TransactionDefinition definition) { }
@Override protected void doCommit(DefaultTransactionStatus status) { }
@Override protected void doRollback(DefaultTransactionStatus status) { }
}
}
@@ -0,0 +1,41 @@
package com.baiyue.lastleg.pda;
import com.baiyue.lastleg.pda.move.controller.PdaInventoryMoveController;
import com.baiyue.lastleg.pda.move.dto.PdaInventoryMoveRequest;
import com.baiyue.lastleg.pda.stocktake.controller.PdaStocktakeController;
import com.baiyue.lastleg.pda.stocktake.dto.PdaStocktakeRequests;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Set;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
class PdaInventoryOperationContractTest {
@Test
void controllersUseOnlyCanonicalPdaIdentityAndRoutes() {
assertThat(PdaStocktakeController.class.getAnnotation(RequestMapping.class).value())
.containsExactly("/api/pda/v1/last-leg/stocktakes");
assertThat(PdaInventoryMoveController.class.getAnnotation(RequestMapping.class).value())
.containsExactly("/api/pda/v1/last-leg/inventory-moves");
assertThat(PdaStocktakeController.class.getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')");
assertThat(PdaInventoryMoveController.class.getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_WAREHOUSE_PICKER')");
}
@Test
void deviceRequestsCannotSupplyIdentityOrWarehouseScope() {
Set<String> forbidden = Set.of("customerId", "warehouseId", "overseasWarehouseId", "operator", "operatorId");
for (Class<?> type : List.of(PdaStocktakeRequests.Create.class, PdaStocktakeRequests.Count.class,
PdaStocktakeRequests.Submit.class, PdaInventoryMoveRequest.class)) {
Set<String> fields = Stream.of(type.getRecordComponents()).map(component -> component.getName())
.collect(Collectors.toSet());
assertThat(fields).doesNotContainAnyElementsOf(forbidden);
}
}
}
@@ -0,0 +1,83 @@
package com.baiyue.lastleg.pda;
import com.baiyue.lastleg.palletops.dto.PalletOperationRequests;
import com.baiyue.lastleg.pda.move.dto.PdaInventoryMoveRequest;
import com.baiyue.lastleg.pda.stocktake.dto.PdaStocktakeRequests;
import com.baiyue.lastleg.relabel.pda.dto.PdaRelabelRequests;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.lang.reflect.RecordComponent;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PdaOperationsOpenApiContractTest {
private static final Set<String> SERVER_OWNED = Set.of(
"customerId", "warehouseId", "overseasWarehouseId", "operator", "operatorKey", "operatorName");
@Test
void pdaOperationRequestsNeverAcceptServerOwnedScope() {
List<Class<?>> containers = List.of(PdaStocktakeRequests.class, PalletOperationRequests.class,
PdaRelabelRequests.class);
for (Class<?> container : containers) {
for (Class<?> type : container.getDeclaredClasses()) {
if (type.isRecord()) assertNoServerOwnedFields(type);
}
}
assertNoServerOwnedFields(PdaInventoryMoveRequest.class);
}
@Test
void versionedOpenApiPublishesEveryPdaWarehouseOperation() throws IOException {
String yaml = resource("openapi/overseas-pda-operations-v1.yaml");
List<String> paths = List.of(
"/api/pda/v1/last-leg/stocktakes:",
"/api/pda/v1/last-leg/stocktakes/{taskNo}:",
"/api/pda/v1/last-leg/stocktakes/{taskNo}/counts:",
"/api/pda/v1/last-leg/stocktakes/{taskNo}/submit:",
"/api/pda/v1/last-leg/inventory-moves:",
"/api/pda/v1/last-leg/pallet-operations/pallets/resolve:",
"/api/pda/v1/last-leg/pallet-operations/assemble:",
"/api/pda/v1/last-leg/pallet-operations/disassemble:",
"/api/pda/v1/last-leg/pallet-operations/swap:",
"/api/pda/v1/last-leg/pallet-operations/unbind:",
"/api/pda/v1/last-leg/relabel-tasks/candidates:",
"/api/pda/v1/last-leg/relabel-tasks/{relabelNo}:",
"/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/claim:",
"/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/units:",
"/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/units/skip:",
"/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/exceptions:",
"/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/photos/files:",
"/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/complete:",
"/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/review:");
for (String path : paths) assertTrue(yaml.contains(path), path);
assertEquals(19, paths.size());
assertTrue(yaml.contains("enum: [LOCATION, PALLET]"));
assertTrue(yaml.contains("enum: [APPROVE, REWORK]"));
assertTrue(yaml.contains("enum: [DAMAGED, MISSING_LABEL, LABEL_MISMATCH, UNREADABLE, OTHER]"));
for (String field : SERVER_OWNED) assertFalse(yaml.contains(field + ":"), field);
for (String excluded : List.of("/outbound", "/dispatch", "/delivery")) {
assertFalse(yaml.contains(excluded), excluded);
}
}
private static void assertNoServerOwnedFields(Class<?> type) {
Set<String> fields = Arrays.stream(type.getRecordComponents())
.map(RecordComponent::getName).collect(Collectors.toSet());
assertTrue(java.util.Collections.disjoint(fields, SERVER_OWNED), type.getSimpleName());
}
private String resource(String name) throws IOException {
try (var input = getClass().getClassLoader().getResourceAsStream(name)) {
assertTrue(input != null, name);
return new String(input.readAllBytes(), StandardCharsets.UTF_8);
}
}
}
@@ -0,0 +1,141 @@
package com.baiyue.lastleg.pda.move.service;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
import com.baiyue.lastleg.inventory.entity.InventoryLedger;
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryLedgerMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway;
import com.baiyue.lastleg.pda.move.dto.PdaInventoryMoveRequest;
import com.baiyue.lastleg.pda.move.entity.PdaInventoryMove;
import com.baiyue.lastleg.pda.move.mapper.PdaInventoryMoveMapper;
import com.baiyue.lastleg.pda.warehouse.service.PdaWarehouseService;
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.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PdaInventoryMoveServiceTest {
@Mock PdaInventoryMoveMapper moves;
@Mock InventoryPositionMapper positions;
@Mock InventoryBalanceMapper balances;
@Mock InventoryLedgerMapper ledgers;
@Mock WarehouseResourceGateway warehouse;
@Mock PalletPhysicalGateway physical;
@Mock PdaWarehouseService history;
private PdaInventoryMoveService service;
@BeforeEach
void setUp() {
service = new PdaInventoryMoveService(); ReflectionTestUtils.setField(service, "moves", moves);
ReflectionTestUtils.setField(service, "positions", positions); ReflectionTestUtils.setField(service, "balances", balances);
ReflectionTestUtils.setField(service, "ledgers", ledgers); ReflectionTestUtils.setField(service, "warehouse", warehouse);
ReflectionTestUtils.setField(service, "physical", physical);
ReflectionTestUtils.setField(service, "history", history);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
}
@Test
void atomicallyRelocatesPhysicalPalletAndEveryWarehouseScopedPosition() {
when(warehouse.resolvePallet("PAL-1")).thenReturn(pallet(900L, 20L, 2));
when(warehouse.resolveLocation(900L, 20L)).thenReturn(location(20L, "A-01"));
when(warehouse.resolveLocation(900L, "B-01")).thenReturn(location(21L, "B-01"));
InventoryPosition position = new InventoryPosition(); position.setId(11L); position.setCustomerId("100");
position.setBalanceId(10L); position.setOverseasWarehouseId("900"); position.setPalletId(30L);
position.setPalletNo("PAL-1"); position.setWarehouseLocationId(20L); position.setWarehouseLocationCode("A-01");
position.setAvailablePackages(5L); position.setHoldPackages(1L); position.setAllocatedPackages(0L);
position.setPickedPackages(0L); position.setReceivingPackages(0L); position.setVersion(4L);
when(moves.lockPositions("900", 30L)).thenReturn(List.of(position));
InventoryBalance balance = new InventoryBalance(); balance.setId(10L); balance.setCustomerId("100");
balance.setOverseasWarehouseId("900"); balance.setInboundOrderId(50L); balance.setInboundCode("IN-1");
when(balances.selectById(10L)).thenReturn(balance);
when(physical.apply(eq(900L), any(PalletPhysicalGateway.PalletReference.class), eq("OCCUPIED"),
eq(21L), anyString(), eq("库位优化"))).thenReturn(transition(false));
var response = service.move(900L, "operator-1",
new PdaInventoryMoveRequest("PAL-1", "B-01", 2, "move-1", "库位优化"));
verify(physical).apply(eq(900L), argThat(value -> value.palletId() == 30L
&& value.locationId() == 20L && value.version() == 2), eq("OCCUPIED"),
eq(21L), anyString(), eq("库位优化"));
ArgumentCaptor<InventoryPosition> updated = ArgumentCaptor.forClass(InventoryPosition.class);
verify(positions).updateById(updated.capture());
assertThat(updated.getValue().getWarehouseLocationId()).isEqualTo(21L);
assertThat(updated.getValue().getWarehouseLocationCode()).isEqualTo("B-01");
assertThat(response.packageCount()).isEqualTo(6L);
assertThat(response.palletVersion()).isEqualTo(3);
assertThat(response.status()).isEqualTo("COMPLETED");
verify(ledgers).insert(any(InventoryLedger.class)); verify(moves).updateById(any(PdaInventoryMove.class));
}
@Test
void rejectsCrossWarehousePalletWithoutMovingResources() {
when(warehouse.resolvePallet("PAL-1")).thenReturn(pallet(901L, 20L, 2));
assertThatThrownBy(() -> service.move(900L, "operator-1",
new PdaInventoryMoveRequest("PAL-1", "B-01", 2, "move-1", "库位优化")))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
verify(physical, never()).apply(anyLong(), any(), anyString(), anyLong(), anyString(), any());
}
@Test
void resumesWhenWarehouseCommittedButLastlegTransactionRolledBack() {
when(warehouse.resolvePallet("PAL-1")).thenReturn(pallet(900L, 21L, 3));
when(warehouse.resolveLocation(900L, "B-01")).thenReturn(location(21L, "B-01"));
when(warehouse.resolveLocation(900L, 20L)).thenReturn(location(20L, "A-01"));
InventoryPosition position = new InventoryPosition(); position.setId(11L); position.setCustomerId("100");
position.setBalanceId(10L); position.setOverseasWarehouseId("900"); position.setPalletId(30L);
position.setPalletNo("PAL-1"); position.setWarehouseLocationId(20L); position.setWarehouseLocationCode("A-01");
position.setAvailablePackages(5L); position.setHoldPackages(0L); position.setAllocatedPackages(0L);
position.setPickedPackages(0L); position.setReceivingPackages(0L); position.setVersion(4L);
when(moves.lockPositions("900", 30L)).thenReturn(List.of(position));
InventoryBalance balance = new InventoryBalance(); balance.setId(10L); balance.setCustomerId("100");
balance.setOverseasWarehouseId("900"); balance.setInboundOrderId(50L); balance.setInboundCode("IN-1");
when(balances.selectById(10L)).thenReturn(balance);
when(physical.apply(eq(900L), any(PalletPhysicalGateway.PalletReference.class), eq("OCCUPIED"),
eq(21L), anyString(), eq("恢复移库"))).thenReturn(transition(true));
var response = service.move(900L, "operator-1",
new PdaInventoryMoveRequest("PAL-1", "B-01", 2, "move-retry", "恢复移库"));
verify(physical).apply(eq(900L), argThat(value -> value.locationId() == 20L && value.version() == 2),
eq("OCCUPIED"), eq(21L), anyString(), eq("恢复移库"));
assertThat(response.sourceLocationCode()).isEqualTo("A-01");
assertThat(response.targetLocationCode()).isEqualTo("B-01");
verify(positions).updateById(any(InventoryPosition.class));
}
private static WarehouseResourceGateway.WarehousePalletReference pallet(long warehouseId, long locationId, int version) {
return new WarehouseResourceGateway.WarehousePalletReference(30L, "PAL-1", warehouseId, locationId,
"OCCUPIED", null, version);
}
private static WarehouseResourceGateway.WarehouseLocationReference location(long id, String code) {
return new WarehouseResourceGateway.WarehouseLocationReference(id, code, "STORAGE", BigDecimal.TEN, "OCCUPIED");
}
private static PalletPhysicalGateway.Transition transition(boolean replayed) {
return new PalletPhysicalGateway.Transition("move-transition", 30L, "PAL-1", 900L,
"OCCUPIED", "OCCUPIED", 20L, 21L, 2, 3, "APPLIED", replayed);
}
}
@@ -0,0 +1,146 @@
package com.baiyue.lastleg.pda.stocktake.service;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryLedgerMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.inventory.entity.InventoryBalance;
import com.baiyue.lastleg.inventory.entity.InventoryLedger;
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
import com.baiyue.lastleg.pda.stocktake.dto.PdaStocktakeRequests;
import com.baiyue.lastleg.pda.stocktake.entity.PdaStocktakeItem;
import com.baiyue.lastleg.pda.stocktake.entity.PdaStocktakeTask;
import com.baiyue.lastleg.pda.stocktake.mapper.PdaStocktakeActionMapper;
import com.baiyue.lastleg.pda.stocktake.mapper.PdaStocktakeInventoryMapper;
import com.baiyue.lastleg.pda.stocktake.mapper.PdaStocktakeItemMapper;
import com.baiyue.lastleg.pda.stocktake.mapper.PdaStocktakeTaskMapper;
import com.baiyue.lastleg.pda.stocktake.model.StocktakeCandidate;
import com.baiyue.lastleg.pda.warehouse.service.PdaWarehouseService;
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.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PdaStocktakeServiceTest {
@Mock PdaStocktakeTaskMapper tasks;
@Mock PdaStocktakeItemMapper items;
@Mock PdaStocktakeActionMapper actions;
@Mock PdaStocktakeInventoryMapper inventory;
@Mock InventoryPositionMapper positions;
@Mock InventoryBalanceMapper balances;
@Mock InventoryLedgerMapper ledgers;
@Mock WarehouseResourceGateway warehouse;
@Mock PdaWarehouseService history;
private PdaStocktakeService service;
@BeforeEach
void setUp() {
service = new PdaStocktakeService();
ReflectionTestUtils.setField(service, "tasks", tasks); ReflectionTestUtils.setField(service, "items", items);
ReflectionTestUtils.setField(service, "actions", actions); ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "positions", positions); ReflectionTestUtils.setField(service, "balances", balances);
ReflectionTestUtils.setField(service, "ledgers", ledgers); ReflectionTestUtils.setField(service, "warehouse", warehouse);
ReflectionTestUtils.setField(service, "history", history);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
}
@Test
void createsWarehouseBoundLocationTaskAndFreezesEveryPosition() {
when(warehouse.resolveLocation(900L, "A-01")).thenReturn(new WarehouseResourceGateway.WarehouseLocationReference(
20L, "A-01", "STORAGE", BigDecimal.TEN, "OCCUPIED"));
StocktakeCandidate candidate = new StocktakeCandidate(); candidate.setCustomerId("100");
candidate.setBalanceId(10L); candidate.setPositionId(11L); candidate.setSkuCode("SKU-1");
candidate.setPalletId(30L); candidate.setPalletCode("PAL-1"); candidate.setLocationId(20L);
candidate.setLocationCode("A-01"); candidate.setBookPackages(8L); candidate.setPositionVersion(3L);
when(items.candidatesByLocation("900", 20L, "A-01")).thenReturn(List.of(candidate));
when(items.views(anyLong())).thenReturn(List.of());
var response = service.create(900L, "operator-1", new PdaStocktakeRequests.Create(
"location", "A-01", "create-1"));
ArgumentCaptor<PdaStocktakeTask> task = ArgumentCaptor.forClass(PdaStocktakeTask.class);
ArgumentCaptor<PdaStocktakeItem> item = ArgumentCaptor.forClass(PdaStocktakeItem.class);
verify(tasks).insert(task.capture()); verify(items).insert(item.capture());
assertThat(task.getValue().getOverseasWarehouseId()).isEqualTo(900L);
assertThat(task.getValue().getScopeType()).isEqualTo("LOCATION");
assertThat(task.getValue().getCreatedBy()).isEqualTo("operator-1");
assertThat(item.getValue().getCustomerId()).isEqualTo("100");
assertThat(item.getValue().getBookPackages()).isEqualTo(8L);
assertThat(item.getValue().getPositionVersion()).isEqualTo(3L);
assertThat(response.task().status()).isEqualTo("IN_PROGRESS");
}
@Test
void rejectsPalletFromAnotherWarehouseBeforeReadingInventory() {
when(warehouse.resolvePallet("PAL-1")).thenReturn(new WarehouseResourceGateway.WarehousePalletReference(
30L, "PAL-1", 901L, 20L, "OCCUPIED", null, 2));
assertThatThrownBy(() -> service.create(900L, "operator-1",
new PdaStocktakeRequests.Create("PALLET", "PAL-1", "create-1")))
.isInstanceOf(ResponseStatusException.class).hasMessageContaining("409");
verify(items, never()).candidatesByPallet(anyString(), anyLong());
}
@Test
void submitsCountedDifferenceAndWritesBalancedAuditLedger() {
PdaStocktakeTask task = new PdaStocktakeTask(); task.setId(40L); task.setTaskNo("CC-40");
task.setOverseasWarehouseId(900L); task.setScopeType("PALLET"); task.setScopeCode("PAL-1");
task.setStatus("IN_PROGRESS"); task.setPositionCount(1); task.setCountedCount(1); task.setDifferenceCount(1);
task.setVersion(2L);
PdaStocktakeTask submitted = new PdaStocktakeTask(); submitted.setId(40L); submitted.setTaskNo("CC-40");
submitted.setOverseasWarehouseId(900L); submitted.setScopeType("PALLET"); submitted.setScopeCode("PAL-1");
submitted.setStatus("SUBMITTED"); submitted.setPositionCount(1); submitted.setCountedCount(1);
submitted.setDifferenceCount(1); submitted.setVersion(3L);
when(tasks.find(900L, "CC-40")).thenReturn(task, submitted);
when(tasks.lock(900L, "CC-40")).thenReturn(task); when(tasks.submit(40L, 2L, "operator-1", "复盘确认")).thenReturn(1);
PdaStocktakeItem item = new PdaStocktakeItem(); item.setId(41L); item.setTaskId(40L); item.setCustomerId("100");
item.setBalanceId(10L); item.setPositionId(11L); item.setSkuCode("SKU-1"); item.setBookPackages(5L);
item.setActualPackages(7L); item.setDifferencePackages(2L); item.setPositionVersion(3L); item.setVersion(1L);
when(items.lockAll(40L)).thenReturn(List.of(item)); when(items.views(40L)).thenReturn(List.of());
InventoryPosition position = new InventoryPosition(); position.setId(11L); position.setCustomerId("100");
position.setBalanceId(10L); position.setOverseasWarehouseId("900"); position.setPalletId(30L);
position.setPalletNo("PAL-1"); position.setAvailablePackages(5L); position.setHoldPackages(0L);
position.setAllocatedPackages(0L); position.setPickedPackages(0L); position.setReceivingPackages(0L);
position.setVersion(3L);
InventoryBalance balance = new InventoryBalance(); balance.setId(10L); balance.setCustomerId("100");
balance.setOverseasWarehouseId("900"); balance.setAvailablePackages(5L); balance.setVersion(8L);
when(inventory.lockPosition(11L, "100", "900")).thenReturn(position);
when(inventory.lockBalance(10L, "100", "900")).thenReturn(balance);
var response = service.submit(900L, "operator-1", "CC-40",
new PdaStocktakeRequests.Submit(2L, "复盘确认", "submit-1"));
ArgumentCaptor<InventoryPosition> changedPosition = ArgumentCaptor.forClass(InventoryPosition.class);
ArgumentCaptor<InventoryBalance> changedBalance = ArgumentCaptor.forClass(InventoryBalance.class);
ArgumentCaptor<InventoryLedger> ledger = ArgumentCaptor.forClass(InventoryLedger.class);
verify(positions).updateById(changedPosition.capture()); verify(balances).updateById(changedBalance.capture());
verify(ledgers).insert(ledger.capture());
assertThat(changedPosition.getValue().getAvailablePackages()).isEqualTo(7L);
assertThat(changedBalance.getValue().getAvailablePackages()).isEqualTo(7L);
assertThat(ledger.getValue().getBeforePackages()).isEqualTo(5L);
assertThat(ledger.getValue().getDeltaPackages()).isEqualTo(2L);
assertThat(ledger.getValue().getAfterPackages()).isEqualTo(7L);
assertThat(ledger.getValue().getBusinessType()).isEqualTo("STOCKTAKE_ADJUST");
assertThat(response.task().status()).isEqualTo("SUBMITTED");
}
}
@@ -0,0 +1,86 @@
package com.baiyue.lastleg.relabel.pda.controller;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.relabel.pda.dto.PdaRelabelRequests;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.transaction.annotation.Transactional;
import java.lang.reflect.RecordComponent;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
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;
class PdaRelabelContractTest {
@Test
void controllerUsesStablePdaPathAndExactPickerAuthority() {
assertEquals("/api/pda/v1/last-leg/relabel-tasks",
PdaRelabelController.class.getAnnotation(RequestMapping.class).value()[0]);
assertEquals("hasAuthority('lastleg:pda:operate')",
PdaRelabelController.class.getAnnotation(PreAuthorize.class).value());
}
@Test
void deviceRequestsNeverAcceptServerOwnedScopeOrOperatorFields() {
Set<String> forbidden = Set.of("customerId", "warehouseId", "overseasWarehouseId",
"operatorId", "operatorName", "tenantId");
for (Class<?> type : PdaRelabelRequests.class.getDeclaredClasses()) {
Set<String> fields = Arrays.stream(type.getRecordComponents()).map(RecordComponent::getName)
.collect(Collectors.toSet());
assertTrue(fields.stream().noneMatch(forbidden::contains), type.getSimpleName() + " exposes server scope");
}
}
@Test
void identityRequiresPlatformPickerPermissionAndFrozenWarehouse() {
AuthenticatedUser valid = user(Set.of("lastleg:pda:operate", "ROLE_WAREHOUSE_PICKER"), 900L);
assertEquals(900L, PdaRelabelController.identity(valid).overseasWarehouseId());
assertThrows(ResponseStatusException.class,
() -> PdaRelabelController.identity(user(Set.of("lastleg:pda:operate"), 900L)));
assertThrows(ResponseStatusException.class,
() -> PdaRelabelController.identity(user(Set.of("lastleg:pda:operate", "ROLE_WAREHOUSE_PICKER"), null)));
}
@Test
void baselineDefinesPdaRelabelFactsWithoutOutboundFields() throws Exception {
String sql = Files.readString(Path.of("src/main/resources/db/lastleg.sql"), StandardCharsets.UTF_8);
for (String table : Set.of("ll_relabel_pda_task", "ll_relabel_pda_unit_fact",
"ll_relabel_pda_operation", "ll_relabel_pda_history")) {
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS " + table));
}
assertTrue(sql.contains("uk_ll_relabel_pda_operation_key (operator_key,idempotency_key)"));
assertTrue(sql.contains("uk_ll_relabel_pda_old_label (old_label)"));
int start = sql.indexOf("CREATE TABLE IF NOT EXISTS ll_relabel_pda_task");
int end = sql.indexOf("CREATE TABLE IF NOT EXISTS ll_dropship", start);
String block = sql.substring(start, end);
assertFalse(block.contains("dispatch_no"));
assertFalse(block.contains("delivery_no"));
assertFalse(block.contains("outbound_order_no"));
}
@Test
void everyStateMutationIsTransactional() throws Exception {
Class<?> service = Class.forName("com.baiyue.lastleg.relabel.pda.service.PdaRelabelService");
for (String method : Set.of("claim", "scan", "skip", "exception", "complete", "review")) {
assertTrue(Arrays.stream(service.getDeclaredMethods())
.filter(candidate -> method.equals(candidate.getName()))
.anyMatch(candidate -> candidate.isAnnotationPresent(Transactional.class)),
method + " must be transactional");
}
}
private static AuthenticatedUser user(Set<String> permissions, Long warehouseId) {
return new AuthenticatedUser(7L, 0L, 0L, 70L, "picker", "PLATFORM", "PDA",
permissions, false, null, null, false, warehouseId);
}
}
@@ -0,0 +1,245 @@
package com.baiyue.lastleg.relabel.pda.service;
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
import com.baiyue.lastleg.relabel.entity.RelabelItem;
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.baiyue.lastleg.relabel.pda.dto.PdaRelabelRequests;
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelOperation;
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask;
import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelUnitFact;
import com.baiyue.lastleg.relabel.pda.gateway.RelabelPhotoFileGateway;
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper;
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelOperationMapper;
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper;
import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelUnitFactMapper;
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
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.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
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.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PdaRelabelServiceTest {
@Mock RelabelApplicationMapper applications;
@Mock RelabelItemMapper items;
@Mock PdaRelabelTaskMapper tasks;
@Mock PdaRelabelUnitFactMapper unitFacts;
@Mock PdaRelabelOperationMapper operations;
@Mock PdaRelabelHistoryMapper histories;
@Mock RelabelApplicationService applicationService;
@Mock RelabelPhotoFileGateway photoFiles;
PdaRelabelService service;
RelabelApplication application;
RelabelItem item;
List<PdaRelabelUnitFact> persistedFacts;
@BeforeEach
void setUp() {
service = new PdaRelabelService();
ReflectionTestUtils.setField(service, "applications", applications);
ReflectionTestUtils.setField(service, "items", items);
ReflectionTestUtils.setField(service, "tasks", tasks);
ReflectionTestUtils.setField(service, "unitFacts", unitFacts);
ReflectionTestUtils.setField(service, "operations", operations);
ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "applicationService", applicationService);
ReflectionTestUtils.setField(service, "photoFiles", photoFiles);
ReflectionTestUtils.setField(service, "sourceObjectMapper", new ObjectMapper());
service.initializeObjectMapper();
application = application("PENDING_PROCESSING");
item = item();
persistedFacts = new ArrayList<>();
lenient().when(operations.find(anyString(), anyString())).thenReturn(null);
lenient().when(items.selectList(any())).thenReturn(List.of(item));
lenient().when(histories.selectList(any())).thenReturn(List.of());
lenient().when(unitFacts.selectList(any())).thenAnswer(call -> List.copyOf(persistedFacts));
lenient().when(unitFacts.insert(any(PdaRelabelUnitFact.class))).thenAnswer(call -> { persistedFacts.add(call.getArgument(0)); return 1; });
lenient().when(tasks.updateWithVersion(any(), anyInt())).thenReturn(1);
}
@Test
void claimUsesFrozenWarehouseAndAdvancesExistingApplicationStateMachine() {
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(null);
ArgumentCaptor<PdaRelabelTask> task = ArgumentCaptor.forClass(PdaRelabelTask.class);
var result = service.claim("7", "picker", 900L, "FBR-1", new PdaRelabelRequests.Claim("claim-1"));
verify(tasks).insert(task.capture());
assertEquals(900L, task.getValue().getOverseasWarehouseId());
assertEquals("100", task.getValue().getCustomerId());
assertEquals(PdaRelabelService.PROCESSING, result.taskStatus());
verify(applicationService).changeStatus(eq("100"), eq("FBR-1"), eq("7"), any(RelabelRequests.ChangeStatus.class));
}
@Test
void crossWarehouseClaimIsHiddenAndCannotCreateTask() {
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
ResponseStatusException error = assertThrows(ResponseStatusException.class,
() -> service.claim("7", "picker", 901L, "FBR-1", new PdaRelabelRequests.Claim("claim-1")));
assertEquals(404, error.getStatusCode().value());
verify(tasks, never()).insert(any(PdaRelabelTask.class));
}
@Test
void unitScanLocksItemToApplicationValidatesNewSkuAndUpdatesVersion() {
application.setStatus("PROCESSING");
PdaRelabelTask task = task("PROCESSING", 0);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(task);
when(items.selectOne(any())).thenReturn(item);
when(unitFacts.findOldLabel("BOX-1")).thenReturn(null);
ArgumentCaptor<PdaRelabelUnitFact> fact = ArgumentCaptor.forClass(PdaRelabelUnitFact.class);
var result = service.scan("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.Scan(10L, "BOX-1", "SKU-NEW", 0, "scan-1"));
verify(unitFacts).insert(fact.capture());
assertEquals("RELABELLED", fact.getValue().getResult());
assertEquals("BOX-1", fact.getValue().getOldLabel());
assertEquals(1, result.relabelledPackages());
assertEquals(1, result.version());
}
@Test
void duplicateOldLabelAndStaleVersionFailBeforeWritingAnotherFact() {
application.setStatus("PROCESSING");
PdaRelabelTask task = task("PROCESSING", 1);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(task);
assertThrows(ResponseStatusException.class, () -> service.skip("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.Skip(10L, "BOX-2", "damaged", 0, "skip-stale")));
verify(unitFacts, never()).insert(any(PdaRelabelUnitFact.class));
}
@Test
void oldLabelAlreadyAttributedToAnotherTaskIsRejected() {
application.setStatus("PROCESSING");
PdaRelabelTask task = task("PROCESSING", 0);
PdaRelabelUnitFact otherTaskFact = fact("RELABELLED");
otherTaskFact.setTaskId(99L);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(task);
when(items.selectOne(any())).thenReturn(item);
when(unitFacts.findOldLabel("BOX-1")).thenReturn(otherTaskFact);
assertThrows(ResponseStatusException.class, () -> service.scan("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.Scan(10L, "BOX-1", "SKU-NEW", 0, "scan-cross-task")));
verify(unitFacts, never()).insert(any(PdaRelabelUnitFact.class));
}
@Test
void completeRequiresEveryUnitAndMovesOnlyPdaTaskToWaitReview() {
application.setStatus("PROCESSING");
PdaRelabelTask task = task("PROCESSING", 1);
task.setRelabelledPackages(1);
persistedFacts.add(fact("RELABELLED"));
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(task);
var result = service.complete("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.Complete(List.of(501L), 1, "complete-1", "done"));
verify(photoFiles).validate("100", 20L, 501L);
assertEquals(PdaRelabelService.WAIT_REVIEW, result.taskStatus());
assertEquals("PROCESSING", result.applicationStatus());
verify(applicationService, never()).changeStatus(anyString(), anyString(), anyString(), any());
}
@Test
void approvalUsesExistingApplicationCompletionAndReworkDoesNot() {
application.setStatus("PROCESSING");
PdaRelabelTask approved = task("WAIT_REVIEW", 2);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(approved);
var result = service.review("8", "reviewer", 900L, "FBR-1",
new PdaRelabelRequests.Review("APPROVE", 2, "review-1", "ok"));
assertEquals("COMPLETED", result.taskStatus());
assertEquals("COMPLETED", result.applicationStatus());
verify(applicationService).changeStatus(eq("100"), eq("FBR-1"), eq("8"), any(RelabelRequests.ChangeStatus.class));
}
@Test
void reviewCanReturnTaskForReworkWithoutCompletingApplication() {
application.setStatus("PROCESSING");
PdaRelabelTask task = task("WAIT_REVIEW", 2);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(task);
var result = service.review("8", "reviewer", 900L, "FBR-1",
new PdaRelabelRequests.Review("REWORK", 2, "review-rework", "wrong label"));
assertEquals("PROCESSING", result.taskStatus());
assertEquals("PROCESSING", result.applicationStatus());
verify(applicationService, never()).changeStatus(anyString(), anyString(), anyString(), any());
}
@Test
void completionRejectsMissingPackageFactsBeforePhotoValidation() {
application.setStatus("PROCESSING");
PdaRelabelTask task = task("PROCESSING", 0);
when(applications.lockAnyByRelabelNo("FBR-1")).thenReturn(application);
when(tasks.lock("100", 1L)).thenReturn(task);
assertThrows(ResponseStatusException.class, () -> service.complete("7", "picker", 900L, "FBR-1",
new PdaRelabelRequests.Complete(List.of(501L), 0, "complete-1", null)));
verify(photoFiles, never()).validate(anyString(), anyLong(), anyLong());
}
private RelabelApplication application(String status) {
RelabelApplication value = new RelabelApplication();
value.setId(1L); value.setCustomerId("100"); value.setRelabelNo("FBR-1");
value.setCustomerOrderNo("ORD-1"); value.setOverseasWarehouseId("900");
value.setStatus(status); value.setItemCount(1); value.setNotifiedPackageCount(1);
value.setLabelDeadline(LocalDateTime.now().plusDays(1));
return value;
}
private RelabelItem item() {
RelabelItem value = new RelabelItem(); value.setId(10L); value.setApplicationId(1L); value.setCustomerId("100");
value.setSkuCode("SKU-OLD"); value.setNewSkuCode("SKU-NEW"); value.setProductNameZh("product");
value.setShipmentNo("SHIP-1"); value.setNotifiedPackageCount(1); return value;
}
private PdaRelabelTask task(String status, int version) {
PdaRelabelTask value = new PdaRelabelTask(); value.setId(20L); value.setApplicationId(1L); value.setCustomerId("100");
value.setRelabelNo("FBR-1"); value.setOverseasWarehouseId(900L); value.setStatus(status);
value.setOperatorKey("7"); value.setOperatorName("picker"); value.setExpectedPackages(1);
value.setRelabelledPackages(0); value.setSkippedPackages(0); value.setExceptionPackages(0);
value.setClaimedAt(LocalDateTime.now()); value.setVersion(version); return value;
}
private PdaRelabelUnitFact fact(String result) {
PdaRelabelUnitFact value = new PdaRelabelUnitFact(); value.setId(30L); value.setTaskId(20L);
value.setApplicationId(1L); value.setItemId(10L); value.setCustomerId("100");
value.setOldLabel("BOX-1"); value.setNewLabel("SKU-NEW"); value.setResult(result);
value.setOperatorName("picker"); value.setOccurredAt(LocalDateTime.now()); return value;
}
}