提交
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package com.baiyue.lastleg.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderRequests;
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
import com.baiyue.lastleg.service.LastlegOrderService;
|
||||
import com.baiyue.lastleg.service.LoadingCompletedConsumer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class LastlegOrderControllerTest {
|
||||
private final LastlegOrderService service = mock(LastlegOrderService.class);
|
||||
private final LastlegOrderController controller = controller();
|
||||
private final LastlegOrderRequests.Query query = new LastlegOrderRequests.Query(
|
||||
1, 20, null, null, null, null, null, null, null, null, null, null);
|
||||
|
||||
@Test
|
||||
void customerCannotOverrideItsTenantScope() {
|
||||
controller.list(user(100L, "MERCHANT"), "200", query);
|
||||
verify(service).page(eq("100"), eq(query));
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformOperatorMustExplicitlyChooseCustomerTenant() {
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> controller.list(user(0L, "PLATFORM"), null, query));
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformOperatorCanQueryExplicitCustomerTenant() {
|
||||
controller.list(user(0L, "PLATFORM"), "200", query);
|
||||
verify(service).page(eq("200"), eq(query));
|
||||
}
|
||||
|
||||
private static AuthenticatedUser user(long tenantId, String appCode) {
|
||||
return new AuthenticatedUser(1L, tenantId, tenantId, 1L, "tester", appCode, "WEB", Set.of());
|
||||
}
|
||||
|
||||
private LastlegOrderController controller() {
|
||||
LastlegOrderController controller = new LastlegOrderController();
|
||||
ReflectionTestUtils.setField(controller, "consumer", mock(LoadingCompletedConsumer.class));
|
||||
ReflectionTestUtils.setField(controller, "orderService", service);
|
||||
ReflectionTestUtils.setField(controller, "tokenGuard", mock(InternalServiceTokenGuard.class));
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.lastleg.inboundplan;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class InboundPlanSchemaContractTest {
|
||||
@Test
|
||||
void planIsUniqueByTenantAndActualContainerNumber() throws IOException {
|
||||
try (var stream = getClass().getClassLoader().getResourceAsStream("db/lastleg.sql")) {
|
||||
assertTrue(stream != null, "lastleg schema must be available");
|
||||
String schema = new String(stream.readAllBytes(), StandardCharsets.UTF_8).toLowerCase();
|
||||
assertTrue(schema.contains("unique key uk_ll_inbound_plan_container (tenant_id, container_no)"));
|
||||
assertTrue(schema.contains("default 'pending_assignment'"));
|
||||
assertTrue(schema.contains("required_package_count"));
|
||||
assertTrue(schema.contains("planned_package_count"));
|
||||
assertTrue(schema.contains("warehouse_resource_lock_id"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_pda_task"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_pda_operation"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_pallet_merge"));
|
||||
assertFalse(schema.contains("cabinet_no"));
|
||||
}
|
||||
}
|
||||
}
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
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.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;
|
||||
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, "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);
|
||||
}
|
||||
|
||||
@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.setTenantId("100");
|
||||
value.setPlanNo("LIP1");
|
||||
value.setStatus("ASSIGNED");
|
||||
return value;
|
||||
}
|
||||
|
||||
private InboundPdaTask task() {
|
||||
InboundPdaTask value = new InboundPdaTask();
|
||||
value.setId(2L);
|
||||
value.setPlanId(1L);
|
||||
value.setTenantId("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;
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package com.baiyue.lastleg.inboundplan.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.mapper.InboundPlanLineMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanOrderMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
|
||||
import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
|
||||
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.transaction.annotation.Transactional;
|
||||
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.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
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 InboundPlanServiceTest {
|
||||
@Mock InboundPlanMapper planMapper;
|
||||
@Mock InboundPlanLineMapper lineMapper;
|
||||
@Mock InboundPlanPalletMapper palletMapper;
|
||||
@Mock InboundPlanPalletAllocationMapper allocationMapper;
|
||||
@Mock InboundPlanOrderMapper orderMapper;
|
||||
|
||||
private InboundPlanService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InboundPlanService();
|
||||
ReflectionTestUtils.setField(service, "planMapper", planMapper);
|
||||
ReflectionTestUtils.setField(service, "lineMapper", lineMapper);
|
||||
ReflectionTestUtils.setField(service, "palletMapper", palletMapper);
|
||||
ReflectionTestUtils.setField(service, "allocationMapper", allocationMapper);
|
||||
ReflectionTestUtils.setField(service, "orderMapper", orderMapper);
|
||||
org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(allocationMapper.selectList(any())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsWholeContainerInOneTransaction() throws Exception {
|
||||
assertNotNull(InboundPlanService.class.getMethod("convert", String.class, Long.class)
|
||||
.getAnnotation(Transactional.class));
|
||||
when(orderMapper.findContainerNo("100", 2L)).thenReturn("CONT-1");
|
||||
when(orderMapper.lockContainerOrders("100", "CONT-1")).thenReturn(List.of(
|
||||
order(1L, "RK-1", "1.5000", "WH-A"),
|
||||
order(2L, "RK-2", "1.0000", "WH-A")));
|
||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
var result = service.convert("100", 2L);
|
||||
|
||||
ArgumentCaptor<InboundPlan> plan = ArgumentCaptor.forClass(InboundPlan.class);
|
||||
verify(planMapper).insert(plan.capture());
|
||||
assertEquals(2, plan.getValue().getLineCount());
|
||||
assertEquals("PENDING_ASSIGNMENT", plan.getValue().getStatus());
|
||||
assertEquals(2, plan.getValue().getPalletCount());
|
||||
assertEquals(new BigDecimal("2.5000"), plan.getValue().getTotalVolumeCbm());
|
||||
verify(palletMapper, times(2)).insert(any(InboundPlanPallet.class));
|
||||
ArgumentCaptor<InboundPlanLine> lines = ArgumentCaptor.forClass(InboundPlanLine.class);
|
||||
verify(lineMapper, times(2)).insert(lines.capture());
|
||||
assertEquals(25, lines.getAllValues().stream().mapToInt(InboundPlanLine::getRequiredPackageCount).sum());
|
||||
ArgumentCaptor<InboundPlanPalletAllocation> allocations =
|
||||
ArgumentCaptor.forClass(InboundPlanPalletAllocation.class);
|
||||
verify(allocationMapper, times(3)).insert(allocations.capture());
|
||||
assertEquals(25, allocations.getAllValues().stream()
|
||||
.mapToInt(InboundPlanPalletAllocation::getPlannedPackageCount).sum());
|
||||
assertEquals(0, allocations.getAllValues().stream()
|
||||
.mapToInt(InboundPlanPalletAllocation::getPickedPackageCount).sum());
|
||||
assertEquals(3, allocations.getAllValues().stream()
|
||||
.filter(item -> "PENDING_COUNT".equals(item.getStatus())).count());
|
||||
verify(orderMapper, times(2)).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(orderMapper, times(2)).insertHistory(any(), any(), any(), any());
|
||||
verify(orderMapper).lockContainerOrders("100", "CONT-1");
|
||||
assertEquals("CONT-1", result.containerNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatedConversionReturnsExistingPlanWithoutWriting() {
|
||||
InboundPlan existing = new InboundPlan();
|
||||
existing.setId(9L);
|
||||
existing.setPlanNo("LIP9");
|
||||
existing.setTenantId("100");
|
||||
existing.setContainerNo("CONT-1");
|
||||
existing.setStatus("PLANNED");
|
||||
existing.setLineCount(1);
|
||||
existing.setPalletCount(1);
|
||||
existing.setTotalVolumeCbm(BigDecimal.ONE);
|
||||
when(orderMapper.findContainerNo("100", 1L)).thenReturn("CONT-1");
|
||||
when(orderMapper.lockContainerOrders("100", "CONT-1")).thenReturn(List.of(order(1L, "RK-1", "1", "WH-A")));
|
||||
when(planMapper.findByContainer("100", "CONT-1")).thenReturn(existing);
|
||||
|
||||
assertEquals("LIP9", service.convert("100", 1L).planNo());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingDestinationWarehouse() {
|
||||
when(orderMapper.findContainerNo("100", 1L)).thenReturn("CONT-1");
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "CONT-1", "RK-1",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0);
|
||||
when(orderMapper.lockContainerOrders("100", "CONT-1")).thenReturn(List.of(invalid));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convert("100", 1L));
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsTransferWarehouseThatDiffersFromCargoSnapshots() {
|
||||
when(orderMapper.findContainerNo("100", 1L)).thenReturn("CONT-1");
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "CONT-1", "RK-1",
|
||||
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, "WH-B", "WH-B", "Warehouse B", 1, 1);
|
||||
when(orderMapper.lockContainerOrders("100", "CONT-1")).thenReturn(List.of(invalid));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convert("100", 1L));
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
}
|
||||
|
||||
private SourceOrderSnapshot order(Long id, String inboundCode, String volume, String warehouse) {
|
||||
return new SourceOrderSnapshot(id, "100", "CONT-1", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
new BigDecimal(volume), new BigDecimal(volume).multiply(BigDecimal.TEN).intValue(),
|
||||
warehouse, warehouse, warehouse, 1, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.baiyue.lastleg.inboundplan.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PalletAllocatorTest {
|
||||
@Test
|
||||
void roundsEachDestinationGroupUpToTwoCbmPallets() {
|
||||
assertEquals(2, allocate("4.0000").pallets().size());
|
||||
assertEquals(3, allocate("4.0100").pallets().size());
|
||||
assertEquals(1, allocate("0.1000").pallets().size());
|
||||
assertEquals(0, allocate("0.0000").pallets().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void neverMixesDestinationWarehousesAndCanSplitOneLine() {
|
||||
PalletAllocator.Result result = PalletAllocator.allocate(List.of(
|
||||
new PalletAllocator.LineInput(1L, "WH-A", new BigDecimal("2.5000"), 5),
|
||||
new PalletAllocator.LineInput(2L, "WH-B", new BigDecimal("0.1000"), 1)));
|
||||
|
||||
assertEquals(3, result.pallets().size());
|
||||
assertEquals(2, result.allocations().stream().filter(item -> item.orderId() == 1L).count());
|
||||
assertTrue(result.allocations().stream().allMatch(item ->
|
||||
item.pallet().warehouseKey().equals(item.orderId() == 1L ? "WH-A" : "WH-B")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void distributesPackagesProportionallyAndAssignsRemainderToLastPallet() {
|
||||
PalletAllocator.Result result = PalletAllocator.allocate(List.of(
|
||||
new PalletAllocator.LineInput(1L, "WH-A", new BigDecimal("4.0100"), 10)));
|
||||
|
||||
assertEquals(List.of(4, 4, 2), result.allocations().stream()
|
||||
.map(PalletAllocator.AllocationDraft::plannedPackageCount).toList());
|
||||
assertEquals(10, result.allocations().stream()
|
||||
.mapToInt(PalletAllocator.AllocationDraft::plannedPackageCount).sum());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotLosePackagesWhenEarlyProportionalSharesRoundDown() {
|
||||
PalletAllocator.Result result = PalletAllocator.allocate(List.of(
|
||||
new PalletAllocator.LineInput(1L, "WH-A", new BigDecimal("4.0100"), 1)));
|
||||
|
||||
assertEquals(List.of(0, 0, 1), result.allocations().stream()
|
||||
.map(PalletAllocator.AllocationDraft::plannedPackageCount).toList());
|
||||
}
|
||||
|
||||
private PalletAllocator.Result allocate(String volume) {
|
||||
return PalletAllocator.allocate(List.of(
|
||||
new PalletAllocator.LineInput(1L, "WH-A", new BigDecimal(volume),
|
||||
new BigDecimal(volume).signum() == 0 ? 0 : 1)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.lastleg.security;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
class InternalServiceTokenGuardTest {
|
||||
@Test void acceptsOnlyConfiguredToken() {
|
||||
InternalServiceTokenGuard guard=new InternalServiceTokenGuard("secret-token");
|
||||
assertDoesNotThrow(()->guard.verify("secret-token"));
|
||||
assertEquals(401,assertThrows(ResponseStatusException.class,()->guard.verify("wrong")).getStatusCode().value());
|
||||
assertEquals(401,assertThrows(ResponseStatusException.class,()->guard.verify(null)).getStatusCode().value());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.IntStream;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
class LastlegOrderNoGeneratorTest {
|
||||
@Test void actualContainerNumberIsStableAndShared() {
|
||||
LastlegOrderNoGenerator generator = new LastlegOrderNoGenerator();
|
||||
assertEquals("LMO2087379674555461633", generator.forContainerNo("2087379674555461633"));
|
||||
assertEquals(generator.forContainerNo("MSCU1"), new LastlegOrderNoGenerator().forContainerNo("MSCU1"));
|
||||
assertNotEquals(generator.forContainerNo("MSCU1"), generator.forContainerNo("CAB1"));
|
||||
assertTrue(generator.forContainerNo("MSCU1234567890").length() <= 32);
|
||||
assertThrows(IllegalArgumentException.class, () -> generator.forContainerNo(" "));
|
||||
}
|
||||
|
||||
@Test void remainsUniqueAcrossInstancesAndConcurrentCalls() throws Exception {
|
||||
LastlegOrderNoGenerator beforeRestart=new LastlegOrderNoGenerator();
|
||||
String first=beforeRestart.next();
|
||||
LastlegOrderNoGenerator afterRestart=new LastlegOrderNoGenerator();
|
||||
assertNotEquals(first,afterRestart.next());
|
||||
ExecutorService pool=Executors.newFixedThreadPool(8);
|
||||
try {
|
||||
var tasks=IntStream.range(0,1000).mapToObj(i->(Callable<String>)afterRestart::next).toList();
|
||||
Set<String> ids=pool.invokeAll(tasks).stream().map(future->{
|
||||
try { return future.get(); } catch(Exception exception) { throw new RuntimeException(exception); }
|
||||
}).collect(java.util.stream.Collectors.toSet());
|
||||
assertEquals(1000,ids.size());
|
||||
} finally { pool.shutdownNow(); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.dto.LastlegOrderRequests;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
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.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.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LastlegOrderServiceTest {
|
||||
@Mock LastlegOrderMapper orders;
|
||||
@Mock LastlegOrderDetailMapper details;
|
||||
@Mock LastlegOrderStatusHistoryMapper histories;
|
||||
LastlegOrderService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new LastlegOrderService();
|
||||
ReflectionTestUtils.setField(service, "orderMapper", orders);
|
||||
ReflectionTestUtils.setField(service, "detailMapper", details);
|
||||
ReflectionTestUtils.setField(service, "historyMapper", histories);
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferUpdatesOrderAndEveryCargoSnapshot() {
|
||||
LastlegOrder order = order(11L, "100", "PENDING_ASSIGNMENT", null);
|
||||
when(orders.selectList(any())).thenReturn(List.of(order));
|
||||
var request = new LastlegOrderRequests.TransferDestination(
|
||||
List.of(11L), "wh-2", "LAX-02", "Los Angeles 02", null);
|
||||
|
||||
assertEquals(1, service.transferDestination("100", request));
|
||||
|
||||
assertEquals("wh-2", order.getTransferWarehouseId());
|
||||
assertEquals("LAX-02", order.getTransferWarehouseCode());
|
||||
assertEquals("DESTINATION_ASSIGNED", order.getCurrentStatus());
|
||||
ArgumentCaptor<LastlegOrderDetail> cargo = ArgumentCaptor.forClass(LastlegOrderDetail.class);
|
||||
verify(details).update(cargo.capture(), any());
|
||||
assertEquals("LAX-02", cargo.getValue().getDestinationWarehouseCode());
|
||||
ArgumentCaptor<LastlegOrderStatusHistory> history = ArgumentCaptor.forClass(LastlegOrderStatusHistory.class);
|
||||
verify(histories).insert(history.capture());
|
||||
assertEquals("PENDING_ASSIGNMENT", history.getValue().getFromStatus());
|
||||
assertEquals("DESTINATION_ASSIGNED", history.getValue().getToStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transferRejectsAnOrderAlreadyBoundToInboundPlan() {
|
||||
LastlegOrder order = order(11L, "100", "DESTINATION_ASSIGNED", "IB-1");
|
||||
when(orders.selectList(any())).thenReturn(List.of(order));
|
||||
var request = new LastlegOrderRequests.TransferDestination(
|
||||
List.of(11L), "wh-2", "LAX-02", "Los Angeles 02", null);
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.transferDestination("100", request));
|
||||
verify(orders, never()).updateById(any(LastlegOrder.class));
|
||||
verify(details, never()).update(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void batchRejectsMissingOrCrossTenantIdsBeforeWriting() {
|
||||
when(orders.selectList(any())).thenReturn(List.of(order(11L, "100", "PENDING_ASSIGNMENT", null)));
|
||||
var request = new LastlegOrderRequests.TransferDestination(
|
||||
List.of(11L, 22L), "wh-2", "LAX-02", "Los Angeles 02", null);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.transferDestination("100", request));
|
||||
assertTrue(error.getStatusCode().is4xxClientError());
|
||||
verify(orders, never()).updateById(any(LastlegOrder.class));
|
||||
}
|
||||
|
||||
private static LastlegOrder order(long id, String tenantId, String status, String inboundOrderNo) {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(id);
|
||||
order.setTenantId(tenantId);
|
||||
order.setCurrentStatus(status);
|
||||
order.setInboundOrderNo(inboundOrderNo);
|
||||
return order;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
import com.baiyue.common.event.*; import com.baiyue.lastleg.entity.*; import com.baiyue.lastleg.mapper.*; import com.fasterxml.jackson.databind.*; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; import org.springframework.test.util.ReflectionTestUtils; import java.math.BigDecimal; import java.time.Instant; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*;
|
||||
@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
|
||||
class LoadingCompletedConsumerTest {
|
||||
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
|
||||
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"objectMapper",json); ReflectionTestUtils.setField(service,"orderNoGenerator",new LastlegOrderNoGenerator()); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); }
|
||||
@Test void createsOneOrderPerInboundAndAllSnapshots(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); verify(orders,times(2)).insert(any(LastlegOrder.class)); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU1".equals(o.getContainerNo()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertEquals(new LastlegOrderNoGenerator().forContainerNo("MSCU1"),c.getAllValues().get(0).getOrderNo()); }
|
||||
@Test void repeatedEventReturnsExistingEffect(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); when(inbox.selectById(event.eventId())).thenReturn(row); when(orders.countContainer("100","MSCU1")).thenReturn(2L); assertEquals(2,service.consume(event)); verify(orders).countContainer("100","MSCU1"); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); }
|
||||
@Test void sameEventWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(row); assertThrows(IllegalArgumentException.class,()->service.consume(event)); }
|
||||
@Test void rejectsEventFromWrongRegion(){ EventEnvelope source=event(); EventEnvelope invalid=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),RegionCode.OVERSEAS,RegionCode.CN,source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),source.payloadHash()); assertThrows(IllegalArgumentException.class,()->service.consume(invalid)); }
|
||||
private EventEnvelope event(){ var item1=new FirstLegLoadingPlanCompletedV1.Item("d-1","SKU-1","产品","Product",5,50,1,new BigDecimal("10"),new BigDecimal("9"),new BigDecimal("12"),new BigDecimal("1.2"),"wh-1","ONT8","FBA ONT8","FBA-1","SHIP-1","MARK-1"); var item2=new FirstLegLoadingPlanCompletedV1.Item("d-2","SKU-2","产品2","Product2",3,30,2,new BigDecimal("8"),new BigDecimal("7"),new BigDecimal("9"),new BigDecimal("0.8"),"wh-2","LAX9","FBA LAX9","FBA-2","SHIP-2","MARK-2"); var p=new FirstLegLoadingPlanCompletedV1("plan-1","LP1","book-1","MSCU1","CAB1","ow-1","洛杉矶海外仓",List.of(new FirstLegLoadingPlanCompletedV1.Order("in-1","root-1","ord-1","FL1","root-o1","RK1","wh-1","ONT8","FBA ONT8",5,50,1,new BigDecimal("10"),new BigDecimal("9"),new BigDecimal("1.2"),List.of(item1)),new FirstLegLoadingPlanCompletedV1.Order("in-2","root-2","ord-2","FL2","root-o2","RK2","wh-2","LAX9","FBA LAX9",3,30,2,new BigDecimal("8"),new BigDecimal("7"),new BigDecimal("0.8"),List.of(item2)))); JsonNode node=json.valueToTree(p); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); return new EventEnvelope("123e4567-e89b-12d3-a456-426614174000",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),RegionCode.CN,RegionCode.OVERSEAS,"100","LOADING_PLAN","plan-1",2,1L,"123e4567-e89b-12d3-a456-426614174001","FIRSTLEG_LOADING_COMPLETED:plan-1:100:1",null,node,hash); }
|
||||
}
|
||||
Reference in New Issue
Block a user