feat: complete overseas pda warehouse operations
This commit is contained in:
+86
@@ -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);
|
||||
}
|
||||
}
|
||||
+245
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user