feat: complete phase 5 overseas fulfillment contracts

This commit is contained in:
2026-09-01 14:24:59 +08:00
parent ffed9bf898
commit 97168b19d8
46 changed files with 2446 additions and 409 deletions
@@ -4,6 +4,8 @@ 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 com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaRequests;
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
import org.junit.jupiter.api.Test;
import java.io.IOException;
@@ -32,6 +34,11 @@ class PdaOperationsOpenApiContractTest {
}
}
assertNoServerOwnedFields(PdaInventoryMoveRequest.class);
assertNoServerOwnedFields(DispatchPdaRequests.Pick.class);
assertNoServerOwnedFields(DispatchPdaRequests.CompletePicking.class);
assertNoServerOwnedFields(DispatchPdaRequests.Depart.class);
assertNoServerOwnedFields(DeliveryRequests.PdaPick.class);
assertNoServerOwnedFields(DeliveryRequests.PdaComplete.class);
}
@Test
@@ -56,16 +63,23 @@ class PdaOperationsOpenApiContractTest {
"/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:");
"/api/pda/v1/last-leg/relabel-tasks/{relabelNo}/review:",
"/api/pda/v1/last-leg/dispatches:",
"/api/pda/v1/last-leg/dispatches/{dispatchNo}:",
"/api/pda/v1/last-leg/dispatches/{dispatchNo}/items/{itemId}/pick:",
"/api/pda/v1/last-leg/dispatches/{dispatchNo}/complete:",
"/api/pda/v1/last-leg/dispatches/{dispatchNo}/depart:",
"/api/pda/v1/last-leg/outbound-tasks:",
"/api/pda/v1/last-leg/outbound-tasks/{taskNo}:",
"/api/pda/v1/last-leg/outbound-tasks/{taskNo}/pick:",
"/api/pda/v1/last-leg/outbound-tasks/{taskNo}/complete:");
for (String path : paths) assertTrue(yaml.contains(path), path);
assertEquals(19, paths.size());
assertEquals(28, 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);
}
assertFalse(yaml.contains("/api/pda/v1/last-leg/delivery"));
}
private static void assertNoServerOwnedFields(Class<?> type) {
@@ -0,0 +1,109 @@
package com.baiyue.lastleg.pda;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.delivery.controller.OutboundPdaController;
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
import com.baiyue.lastleg.delivery.dto.DeliveryResponses;
import com.baiyue.lastleg.delivery.service.DeliveryTransferService;
import com.baiyue.lastleg.dispatch.pda.controller.DispatchPdaController;
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaRequests;
import com.baiyue.lastleg.dispatch.pda.service.DispatchPdaWorkflowService;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ResponseStatusException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Set;
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.Mockito.mock;
import static org.mockito.Mockito.verify;
class PdaOutboundDispatchContractTest {
private static final String SECURITY =
"hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')";
@Test
void controllersRequirePdaRoleAndNeverAcceptCustomerOrWarehouseScope() {
assertEquals(SECURITY, DispatchPdaController.class.getAnnotation(PreAuthorize.class).value());
assertEquals(SECURITY, OutboundPdaController.class.getAnnotation(PreAuthorize.class).value());
for (Class<?> controller : Set.of(DispatchPdaController.class, OutboundPdaController.class)) {
for (Method method : controller.getDeclaredMethods()) {
assertTrue(Arrays.stream(method.getParameters()).noneMatch(parameter -> {
RequestParam query = parameter.getAnnotation(RequestParam.class);
return query != null && Set.of("customerId", "warehouseId")
.contains(query.name().isBlank() ? query.value() : query.name());
}));
}
}
assertTrue(Arrays.stream(DispatchPdaRequests.Pick.class.getRecordComponents())
.noneMatch(component -> "warehouseId".equals(component.getName()) || "customerId".equals(component.getName())));
assertTrue(Arrays.stream(DeliveryRequests.PdaPick.class.getRecordComponents())
.noneMatch(component -> "warehouseId".equals(component.getName()) || "customerId".equals(component.getName())));
}
@Test
void controllersPassOnlyTokenBoundWarehouseToServices() {
DispatchPdaWorkflowService dispatchService = mock(DispatchPdaWorkflowService.class);
DeliveryTransferService outboundService = mock(DeliveryTransferService.class);
DispatchPdaController dispatch = new DispatchPdaController();
OutboundPdaController outbound = new OutboundPdaController();
ReflectionTestUtils.setField(dispatch, "service", dispatchService);
ReflectionTestUtils.setField(outbound, "service", outboundService);
AuthenticatedUser user = pda(Set.of("lastleg:pda:operate", "ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR"), 900L);
dispatch.list(user, null);
outbound.list(user, null);
verify(dispatchService).list("900", null);
verify(outboundService).pdaTasks("900", null);
}
@Test
void missingRolePermissionOrWarehouseBindingIsRejected() {
DispatchPdaController controller = new DispatchPdaController();
assertThrows(ResponseStatusException.class,
() -> controller.list(pda(Set.of("lastleg:pda:operate"), 900L), null));
assertThrows(ResponseStatusException.class,
() -> controller.list(pda(Set.of("ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR"), 900L), null));
assertThrows(ResponseStatusException.class,
() -> controller.list(pda(Set.of("lastleg:pda:operate", "ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR"), null), null));
}
@Test
void pdaOpenApiContainsOnlyFrozenDispatchAndOutboundWorkflow() throws Exception {
String yaml = Files.readString(Path.of("src/main/resources/openapi/overseas-pda-operations-v1.yaml"),
StandardCharsets.UTF_8);
assertTrue(yaml.contains("/api/pda/v1/last-leg/dispatches:"));
assertTrue(yaml.contains("/api/pda/v1/last-leg/dispatches/{dispatchNo}/depart:"));
assertTrue(yaml.contains("/api/pda/v1/last-leg/outbound-tasks/{taskNo}/complete:"));
assertFalse(yaml.contains("name: customerId"));
assertFalse(yaml.contains("name: warehouseId"));
assertFalse(yaml.contains("/api/pda/v1/last-leg/delivery"));
assertTrue(yaml.contains("inventoryPositionId:"));
assertTrue(yaml.contains("cartonMark:"));
assertTrue(yaml.contains("availablePackages:"));
assertTrue(yaml.contains("pickedPackages:"));
assertEquals(Set.of("inventoryPositionId", "palletNo", "cartonMark", "skuCode", "inboundCode",
"availablePackages", "pickedPackages"),
Arrays.stream(DeliveryResponses.PdaPickPosition.class.getRecordComponents())
.map(component -> component.getName()).collect(java.util.stream.Collectors.toSet()));
assertEquals(Set.of("idempotencyKey"),
Arrays.stream(DeliveryRequests.PdaComplete.class.getRecordComponents())
.map(component -> component.getName()).collect(java.util.stream.Collectors.toSet()));
}
private static AuthenticatedUser pda(Set<String> permissions, Long warehouseId) {
return new AuthenticatedUser(7L, 0L, 0L, 8L, "picker", "PLATFORM", "PDA", permissions,
false, null, null, false, warehouseId);
}
}