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
@@ -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) { }
}
}