装柜回归测试
This commit is contained in:
+78
@@ -0,0 +1,78 @@
|
||||
package com.baiyue.warehouse.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.PalletAllocationDtos;
|
||||
import com.baiyue.warehouse.service.PalletLocationAllocationService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class InternalPalletLocationControllerTest {
|
||||
private static final String INTERNAL_TOKEN = "test-internal-service-token";
|
||||
|
||||
private final PalletLocationAllocationService service = mock(PalletLocationAllocationService.class);
|
||||
private final InternalPalletLocationController controller = new InternalPalletLocationController();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(controller, "service", service);
|
||||
ReflectionTestUtils.setField(controller, "internalToken", INTERNAL_TOKEN);
|
||||
controller.initializeToken();
|
||||
}
|
||||
|
||||
@Test
|
||||
void transfersOnlyForInternalCaller() {
|
||||
PalletAllocationDtos.TransferRequest request = request();
|
||||
List<PalletAllocationDtos.TransferResult> expected = List.of(new PalletAllocationDtos.TransferResult(
|
||||
101L, "LR101", 201L, 1, 10001L, 20L, 30L, "TP-001", null,
|
||||
40L, "A-01", null, 5, "OCCUPIED"));
|
||||
when(service.transfer(request)).thenReturn(expected);
|
||||
|
||||
var response = controller.transfer(INTERNAL_TOKEN, request);
|
||||
|
||||
assertThat(response.data()).isEqualTo(expected);
|
||||
verify(service).transfer(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidInternalTokenBeforeTransfer() {
|
||||
assertThatThrownBy(() -> controller.transfer("invalid", request()))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(exception -> ((BizException) exception).getResultCode())
|
||||
.isEqualTo(ResultCode.FORBIDDEN);
|
||||
verifyNoInteractions(service);
|
||||
}
|
||||
|
||||
@Test
|
||||
void settlesPdaResourcesOnlyForInternalCaller() {
|
||||
PalletAllocationDtos.SettlementRequest request = settlementRequest();
|
||||
PalletAllocationDtos.SettlementResult expected = new PalletAllocationDtos.SettlementResult(List.of(), List.of());
|
||||
when(service.settle(request)).thenReturn(expected);
|
||||
|
||||
var response = controller.settle(INTERNAL_TOKEN, request);
|
||||
|
||||
assertThat(response.data()).isEqualTo(expected);
|
||||
verify(service).settle(request);
|
||||
}
|
||||
|
||||
private static PalletAllocationDtos.TransferRequest request() {
|
||||
return new PalletAllocationDtos.TransferRequest(10001L, "FIRSTLEG", 88L, 89L,
|
||||
"RK-001-S1", "transfer-88-89", 9001L, List.of(new PalletAllocationDtos.TransferItem(40L, 5)));
|
||||
}
|
||||
|
||||
private static PalletAllocationDtos.SettlementRequest settlementRequest() {
|
||||
return new PalletAllocationDtos.SettlementRequest(0L, 20L, "firstleg-service", 88L, 89L,
|
||||
20, 12, "RK-001-S1", "PDA-SETTLEMENT-88-1", 9001L, "PDA settlement", List.of(101L),
|
||||
List.of(new PalletAllocationDtos.SettlementTransferItem(102L, 12)));
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.warehouse.dto;
|
||||
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PalletAllocationDtosValidationTest {
|
||||
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||
|
||||
@Test
|
||||
void acceptsPublicResourceScope() {
|
||||
LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(30);
|
||||
|
||||
assertThat(validator.validate(new PalletAllocationDtos.ReserveRequest(0L, 20L, "FIRSTLEG", 88L,
|
||||
"RK-001", "measure-88", 9L, BigDecimal.ONE, BigDecimal.ONE, "STANDARD", expiresAt, true))).isEmpty();
|
||||
assertThat(validator.validate(new PalletAllocationDtos.ConfirmRequest(0L, 20L, "FIRSTLEG", 88L,
|
||||
9L, BigDecimal.ONE, BigDecimal.ONE))).isEmpty();
|
||||
assertThat(validator.validate(new PalletAllocationDtos.ReleaseRequest(0L, "FIRSTLEG", 88L, 9L,
|
||||
"入库补偿"))).isEmpty();
|
||||
assertThat(validator.validate(new PalletAllocationDtos.SelectiveReleaseRequest(0L, "FIRSTLEG", 88L,
|
||||
"pda-release-88-1", 9L, "PDA 部分拣货", List.of(40L)))).isEmpty();
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.warehouse.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PalletLocationReleaseIdempotencySqlContractTest {
|
||||
@Test
|
||||
void containsPersistentSelectedReleaseIdempotencySchema() throws IOException {
|
||||
String moduleSql = Files.readString(Path.of("src", "main", "resources", "db",
|
||||
"phase3-pallet-location-release-idempotency.sql"));
|
||||
String unifiedSql = Files.readString(Path.of("..", "deploy", "sql",
|
||||
"头程数据库统一增量-MySQL8.sql"));
|
||||
|
||||
for (String sql : new String[] {moduleSql, unifiedSql}) {
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS wh_pallet_location_release_operation")
|
||||
.contains("idempotency_key VARCHAR(128) NOT NULL")
|
||||
.contains("request_hash CHAR(64) NOT NULL")
|
||||
.contains("response_snapshot JSON NULL")
|
||||
.contains("uk_pallet_location_release_idempotency")
|
||||
.contains("tenant_id, source_service, business_id, idempotency_key");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void containsAtomicPdaSettlementSchema() throws IOException {
|
||||
String moduleSql = Files.readString(Path.of("src", "main", "resources", "db",
|
||||
"phase3-pallet-location-settlement-idempotency.sql"));
|
||||
String unifiedSql = Files.readString(Path.of("..", "deploy", "sql",
|
||||
"头程数据库统一增量-MySQL8.sql"));
|
||||
|
||||
for (String sql : new String[] {moduleSql, unifiedSql}) {
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS wh_pallet_location_settlement_operation")
|
||||
.contains("source_business_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("target_business_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("response_snapshot JSON NULL")
|
||||
.contains("uk_pallet_location_settlement_idempotency")
|
||||
.contains("tenant_id, source_service, source_business_id, idempotency_key")
|
||||
.contains("DROP INDEX uk_pallet_location_settlement_idempotency");
|
||||
}
|
||||
}
|
||||
}
|
||||
+105
-4
@@ -146,12 +146,17 @@ class LocationResourceServiceTest {
|
||||
occupancy(201L, 101L, 9L, "A1", 501L),
|
||||
occupancy(202L, 102L, 10L, "A2", 502L),
|
||||
occupancy(203L, 103L, 11L, "A3", 503L));
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(active);
|
||||
when(repository.findOwned(10001L, 88L, 10L)).thenReturn(occupiedLocation(10L, "A2"));
|
||||
active.forEach(item -> {
|
||||
item.setTenantId(0L);
|
||||
item.setSourceService("firstleg-service");
|
||||
item.setBusinessId(901L);
|
||||
});
|
||||
when(repository.lockOccupiedByBusiness(0L, "firstleg-service", 901L)).thenReturn(active);
|
||||
when(repository.findOwned(0L, 88L, 10L)).thenReturn(occupiedLocation(10L, "A2"));
|
||||
when(repository.releaseOccupancy(active.get(1), "PDA partial pick")).thenReturn(1);
|
||||
when(repository.changeStatus(10001L, 88L, 10L, "OCCUPIED", "AVAILABLE", 0, 7L)).thenReturn(1);
|
||||
when(repository.changeStatus(0L, 88L, 10L, "OCCUPIED", "AVAILABLE", 0, 7L)).thenReturn(1);
|
||||
|
||||
var result = service.releaseSelected(new SelectiveReleaseCommand(10001L, "FIRSTLEG", 301L, 7L,
|
||||
var result = service.releaseSelected(new SelectiveReleaseCommand(0L, "firstleg-service", 901L, 7L,
|
||||
"PDA partial pick", 10, List.of(10L), null));
|
||||
|
||||
assertThat(result).singleElement().satisfies(item -> {
|
||||
@@ -163,6 +168,56 @@ class LocationResourceServiceTest {
|
||||
verify(repository, never()).releaseOccupancy(active.get(2), "PDA partial pick");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReleaseSelectedLocationsWithPersistedAllocatedPackageCount() {
|
||||
LocationOccupancyEntity first = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
first.setAllocatedPackageCount(3);
|
||||
LocationOccupancyEntity second = occupancy(202L, 102L, 10L, "A2", 502L);
|
||||
second.setAllocatedPackageCount(7);
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(first, second));
|
||||
when(repository.findOwned(10001L, 88L, 10L)).thenReturn(occupiedLocation(10L, "A2"));
|
||||
when(repository.releaseOccupancy(second, "PDA full pick")).thenReturn(1);
|
||||
when(repository.changeStatus(10001L, 88L, 10L, "OCCUPIED", "AVAILABLE", 0, 7L)).thenReturn(1);
|
||||
|
||||
var result = service.releaseSelectedExactly(new com.baiyue.warehouse.dto.LocationDtos.ExactSelectiveReleaseCommand(
|
||||
10001L, "FIRSTLEG", 301L, 7L, "PDA full pick", List.of(10L)));
|
||||
|
||||
assertThat(result).singleElement().satisfies(item -> {
|
||||
assertThat(item.locationId()).isEqualTo(10L);
|
||||
assertThat(item.allocatedPackageCount()).isEqualTo(7);
|
||||
});
|
||||
verify(repository, never()).releaseOccupancy(first, "PDA full pick");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReleasePublicResourcesInAllocationOrderWhenLocationsAreNotSpecified() {
|
||||
LocationOccupancyEntity allocationThree = occupancy(203L, 103L, 11L, "A3", 503L);
|
||||
LocationOccupancyEntity allocationOne = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
LocationOccupancyEntity allocationTwo = occupancy(202L, 102L, 10L, "A2", 502L);
|
||||
List<LocationOccupancyEntity> active = List.of(allocationThree, allocationOne, allocationTwo);
|
||||
active.forEach(item -> {
|
||||
item.setTenantId(0L);
|
||||
item.setSourceService("firstleg-service");
|
||||
item.setBusinessId(901L);
|
||||
});
|
||||
when(repository.lockOccupiedByBusiness(0L, "firstleg-service", 901L)).thenReturn(active);
|
||||
for (LocationOccupancyEntity item : List.of(allocationOne, allocationTwo)) {
|
||||
when(repository.findOwned(0L, 88L, item.getLocationId()))
|
||||
.thenReturn(occupiedLocation(item.getLocationId(), item.getLocationCode()));
|
||||
when(repository.releaseOccupancy(item, "PDA default pick")).thenReturn(1);
|
||||
when(repository.changeStatus(0L, 88L, item.getLocationId(), "OCCUPIED", "AVAILABLE", 0, 7L))
|
||||
.thenReturn(1);
|
||||
}
|
||||
|
||||
var result = service.releaseSelected(new SelectiveReleaseCommand(0L, "firstleg-service", 901L, 7L,
|
||||
"PDA default pick", 8, List.of(), 2));
|
||||
|
||||
assertThat(result).extracting(item -> item.allocationNo()).containsExactly(1, 2);
|
||||
assertThat(result).extracting(item -> item.locationCode()).containsExactly("A1", "A2");
|
||||
assertThat(result).extracting(item -> item.releasedPackageCount()).containsExactly(3, 3);
|
||||
verify(repository, never()).releaseOccupancy(allocationThree, "PDA default pick");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReleaseFirstNAllocationsWhenLocationsAreNotSpecified() {
|
||||
List<LocationOccupancyEntity> active = List.of(
|
||||
@@ -213,6 +268,52 @@ class LocationResourceServiceTest {
|
||||
any(String.class), any(String.class), anyInt(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTransferConfirmedOccupiedResourcesToChildInbound() {
|
||||
LocationOccupancyEntity occupancy = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
occupancy.setAllocatedPackageCount(8);
|
||||
occupancy.setAllocatedVolumeCbm(new BigDecimal("1.200000"));
|
||||
LocationReservationEntity reservation = reservation("CONFIRMED");
|
||||
reservation.setReservationNo("LR101");
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(occupancy));
|
||||
when(repository.lockReservation(10001L, 101L)).thenReturn(reservation);
|
||||
when(repository.transferConfirmedReservation(reservation, "FIRSTLEG", 301L, 302L, "RK001-S1"))
|
||||
.thenReturn(1);
|
||||
when(repository.transferOccupied(occupancy, "FIRSTLEG", 301L, 302L, "RK001-S1", 5))
|
||||
.thenReturn(1);
|
||||
|
||||
var result = service.transferToChild(new com.baiyue.warehouse.dto.LocationDtos.TransferCommand(
|
||||
10001L, "FIRSTLEG", 301L, 302L, "RK001-S1", 7L,
|
||||
List.of(new com.baiyue.warehouse.dto.LocationDtos.TransferItem(9L, 5))));
|
||||
|
||||
assertThat(result).singleElement().satisfies(item -> {
|
||||
assertThat(item.reservationNo()).isEqualTo("LR101");
|
||||
assertThat(item.occupancyId()).isEqualTo(201L);
|
||||
assertThat(item.remainingPackageCount()).isEqualTo(5);
|
||||
assertThat(item.state()).isEqualTo("OCCUPIED");
|
||||
});
|
||||
verify(repository).transferConfirmedReservation(reservation, "FIRSTLEG", 301L, 302L, "RK001-S1");
|
||||
verify(repository).transferOccupied(occupancy, "FIRSTLEG", 301L, 302L, "RK001-S1", 5);
|
||||
verify(repository, never()).releaseOccupancy(any(), any(String.class));
|
||||
verify(repository, never()).releaseReservation(any(), any(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectTransferWhenRemainingPackagesExceedAllocation() {
|
||||
LocationOccupancyEntity occupancy = occupancy(201L, 101L, 9L, "A1", 501L);
|
||||
occupancy.setAllocatedPackageCount(4);
|
||||
LocationReservationEntity reservation = reservation("CONFIRMED");
|
||||
when(repository.lockOccupiedByBusiness(10001L, "FIRSTLEG", 301L)).thenReturn(List.of(occupancy));
|
||||
when(repository.lockReservation(10001L, 101L)).thenReturn(reservation);
|
||||
|
||||
assertThatThrownBy(() -> service.transferToChild(new com.baiyue.warehouse.dto.LocationDtos.TransferCommand(
|
||||
10001L, "FIRSTLEG", 301L, 302L, "RK001-S1", 7L,
|
||||
List.of(new com.baiyue.warehouse.dto.LocationDtos.TransferItem(9L, 5)))))
|
||||
.isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
||||
verify(repository, never()).transferConfirmedReservation(any(), any(String.class), anyLong(), anyLong(), any());
|
||||
verify(repository, never()).transferOccupied(any(), any(String.class), anyLong(), anyLong(), any(), anyInt());
|
||||
}
|
||||
|
||||
private static ReserveCommand reserveCommand() {
|
||||
return new ReserveCommand(10001L, 88L, "FIRSTLEG", 301L, "RK001", 7L,
|
||||
LocalDateTime.now().plusMinutes(30), List.of(
|
||||
|
||||
+302
-4
@@ -4,7 +4,14 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.LocationDtos;
|
||||
import com.baiyue.warehouse.dto.PalletAllocationDtos;
|
||||
import com.baiyue.warehouse.entity.PalletEntity;
|
||||
import com.baiyue.warehouse.entity.PalletLocationTransferOperationEntity;
|
||||
import com.baiyue.warehouse.entity.PalletLocationReleaseOperationEntity;
|
||||
import com.baiyue.warehouse.entity.PalletLocationSettlementOperationEntity;
|
||||
import com.baiyue.warehouse.repository.PalletRepository;
|
||||
import com.baiyue.warehouse.repository.PalletLocationTransferOperationRepository;
|
||||
import com.baiyue.warehouse.repository.PalletLocationReleaseOperationRepository;
|
||||
import com.baiyue.warehouse.repository.PalletLocationSettlementOperationRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
@@ -19,20 +26,28 @@ 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.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(classes = {PalletLocationAllocationService.class, PalletLocationAllocationServiceTest.TestConfig.class})
|
||||
class PalletLocationAllocationServiceTest {
|
||||
@MockBean private PalletRepository pallets;
|
||||
@MockBean private LocationResourceService locations;
|
||||
@MockBean private PalletLocationTransferOperationRepository transferOperations;
|
||||
@MockBean private PalletLocationReleaseOperationRepository releaseOperations;
|
||||
@MockBean private PalletLocationSettlementOperationRepository settlementOperations;
|
||||
@Resource private PalletLocationAllocationService service;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfig {
|
||||
@Bean PalletVolumeSplitter splitter() { return new PalletVolumeSplitter(); }
|
||||
@Bean ObjectMapper objectMapper() { return new ObjectMapper(); }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -106,21 +121,304 @@ class PalletLocationAllocationServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldReleaseOnlyPalletsReturnedBySelectedLocationRelease() {
|
||||
var resource = new LocationDtos.SelectiveReleaseResult(
|
||||
var resource = new LocationDtos.ExactSelectiveReleaseResult(
|
||||
102L, 202L, 2, 20L, 30L, 40L, "A-02", 4);
|
||||
when(locations.releaseSelected(any())).thenReturn(List.of(resource));
|
||||
PalletLocationReleaseOperationEntity operation = new PalletLocationReleaseOperationEntity();
|
||||
operation.setId(701L);
|
||||
operation.setOperationStatus("PROCESSING");
|
||||
when(releaseOperations.createIfAbsent(anyLong(), anyString(), anyLong(), anyString(), anyString()))
|
||||
.thenAnswer(invocation -> {
|
||||
operation.setRequestHash(invocation.getArgument(4));
|
||||
return true;
|
||||
});
|
||||
when(releaseOperations.lockByKey(anyLong(), anyString(), anyLong(), anyString())).thenReturn(operation);
|
||||
when(locations.releaseSelectedExactly(any())).thenReturn(List.of(resource));
|
||||
when(pallets.transitionStatus(10001L, 20L, 30L, List.of("OCCUPIED"), "AVAILABLE"))
|
||||
.thenReturn(1);
|
||||
|
||||
var result = service.releaseSelected(new PalletAllocationDtos.SelectiveReleaseRequest(
|
||||
10001L, "FIRSTLEG", 88L, 9001L, "PDA partial pick", 10, List.of(40L), null));
|
||||
10001L, "FIRSTLEG", 88L, "pda-release-88-202", 9001L, "PDA partial pick", List.of(40L)));
|
||||
|
||||
assertThat(result).singleElement().satisfies(item -> {
|
||||
assertThat(item.allocationNo()).isEqualTo(2);
|
||||
assertThat(item.locationId()).isEqualTo(40L);
|
||||
assertThat(item.releasedPackageCount()).isEqualTo(4);
|
||||
assertThat(item.allocatedPackageCount()).isEqualTo(4);
|
||||
});
|
||||
verify(pallets).transitionStatus(10001L, 20L, 30L, List.of("OCCUPIED"), "AVAILABLE");
|
||||
verify(releaseOperations).complete(org.mockito.ArgumentMatchers.eq(701L), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReplayCompletedSelectedReleaseForSameIdempotencyKey() throws Exception {
|
||||
var expected = List.of(new PalletAllocationDtos.SelectiveReleaseResult(101L, 201L, 1,
|
||||
30L, 40L, "A-01", 6));
|
||||
PalletLocationReleaseOperationEntity operation = new PalletLocationReleaseOperationEntity();
|
||||
operation.setOperationStatus("COMPLETED");
|
||||
operation.setResponseSnapshot(new ObjectMapper().writeValueAsString(expected));
|
||||
String[] requestHash = new String[1];
|
||||
when(releaseOperations.createIfAbsent(anyLong(), anyString(), anyLong(), anyString(), anyString()))
|
||||
.thenAnswer(invocation -> {
|
||||
requestHash[0] = invocation.getArgument(4);
|
||||
return false;
|
||||
});
|
||||
when(releaseOperations.lockByKey(anyLong(), anyString(), anyLong(), anyString()))
|
||||
.thenAnswer(invocation -> {
|
||||
operation.setRequestHash(requestHash[0]);
|
||||
return operation;
|
||||
});
|
||||
|
||||
assertThat(service.releaseSelected(selectiveReleaseRequest())).isEqualTo(expected);
|
||||
verify(locations, never()).releaseSelectedExactly(any());
|
||||
verify(releaseOperations, never()).complete(anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectSelectedReleaseWhenIdempotencyKeyHasDifferentRequest() {
|
||||
PalletLocationReleaseOperationEntity operation = new PalletLocationReleaseOperationEntity();
|
||||
operation.setOperationStatus("COMPLETED");
|
||||
operation.setRequestHash("different-request-hash");
|
||||
when(releaseOperations.createIfAbsent(anyLong(), anyString(), anyLong(), anyString(), anyString()))
|
||||
.thenReturn(false);
|
||||
when(releaseOperations.lockByKey(anyLong(), anyString(), anyLong(), anyString())).thenReturn(operation);
|
||||
|
||||
assertThatThrownBy(() -> service.releaseSelected(selectiveReleaseRequest())).isInstanceOf(BizException.class);
|
||||
verify(locations, never()).releaseSelectedExactly(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSettlePdaResourcesAtomicallyAndPersistCombinedSnapshot() {
|
||||
PalletLocationSettlementOperationEntity operation = new PalletLocationSettlementOperationEntity();
|
||||
operation.setId(801L);
|
||||
operation.setOperationStatus("PROCESSING");
|
||||
when(locations.lockOccupiedResources(0L, "firstleg-service", 88L)).thenReturn(sourceResources());
|
||||
when(settlementOperations.createIfAbsent(anyLong(), anyLong(), anyString(), anyString()))
|
||||
.thenAnswer(invocation -> {
|
||||
operation.setRequestHash(invocation.getArgument(3));
|
||||
return true;
|
||||
});
|
||||
when(settlementOperations.lockByKey(anyLong(), anyString()))
|
||||
.thenReturn(operation);
|
||||
when(locations.releaseSelectedReservations(any())).thenReturn(List.of(new LocationDtos.ExactSelectiveReleaseResult(
|
||||
101L, 201L, 1, 20L, 30L, 40L, "A-01", 8)));
|
||||
when(pallets.transitionStatus(0L, 20L, 30L, List.of("OCCUPIED"), "AVAILABLE")).thenReturn(1);
|
||||
when(locations.transferToChildByReservation(any())).thenReturn(List.of(new LocationDtos.TransferResult(
|
||||
102L, "LR102", 202L, 2, 20L, 31L, "TP-002", 41L, "A-02",
|
||||
new BigDecimal("1.200000"), 12, "OCCUPIED")));
|
||||
PalletEntity pallet = pallet(31L, "TP-002");
|
||||
pallet.setStatus("OCCUPIED");
|
||||
pallet.setCapacityCbm(new BigDecimal("1.500000"));
|
||||
when(pallets.findOwned(0L, 20L, 31L)).thenReturn(pallet);
|
||||
|
||||
var result = service.settle(settlementRequest());
|
||||
|
||||
assertThat(result.released()).singleElement().satisfies(item ->
|
||||
assertThat(item.allocatedPackageCount()).isEqualTo(8));
|
||||
assertThat(result.transferred()).singleElement().satisfies(item ->
|
||||
assertThat(item.remainingPackageCount()).isEqualTo(12));
|
||||
verify(settlementOperations).complete(org.mockito.ArgumentMatchers.eq(801L), anyString());
|
||||
org.mockito.ArgumentCaptor<LocationDtos.ReservationReleaseCommand> releaseCommand =
|
||||
org.mockito.ArgumentCaptor.forClass(LocationDtos.ReservationReleaseCommand.class);
|
||||
verify(locations).releaseSelectedReservations(releaseCommand.capture());
|
||||
assertThat(releaseCommand.getValue().tenantId()).isZero();
|
||||
assertThat(releaseCommand.getValue().sourceService()).isEqualTo("firstleg-service");
|
||||
assertThat(releaseCommand.getValue().reservationIds()).containsExactly(101L);
|
||||
org.mockito.ArgumentCaptor<LocationDtos.ReservationTransferCommand> transferCommand =
|
||||
org.mockito.ArgumentCaptor.forClass(LocationDtos.ReservationTransferCommand.class);
|
||||
verify(locations).transferToChildByReservation(transferCommand.capture());
|
||||
assertThat(transferCommand.getValue().items()).extracting(LocationDtos.ReservationTransferItem::reservationId)
|
||||
.containsExactly(102L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReplayCompletedPdaSettlementForSameIdempotencyKey() throws Exception {
|
||||
var expected = new PalletAllocationDtos.SettlementResult(
|
||||
List.of(new PalletAllocationDtos.SelectiveReleaseResult(101L, 201L, 1, 30L, 40L, "A-01", 8)),
|
||||
List.of(new PalletAllocationDtos.TransferResult(102L, "LR102", 202L, 2,
|
||||
0L, 20L, 31L, "TP-002", new BigDecimal("1.5"), 41L, "A-02",
|
||||
new BigDecimal("1.2"), 12, "OCCUPIED")));
|
||||
PalletLocationSettlementOperationEntity operation = new PalletLocationSettlementOperationEntity();
|
||||
operation.setOperationStatus("COMPLETED");
|
||||
operation.setResponseSnapshot(new ObjectMapper().writeValueAsString(expected));
|
||||
String[] requestHash = new String[1];
|
||||
when(locations.lockOccupiedResources(0L, "firstleg-service", 88L)).thenReturn(sourceResources());
|
||||
when(settlementOperations.createIfAbsent(anyLong(), anyLong(), anyString(), anyString()))
|
||||
.thenAnswer(invocation -> {
|
||||
requestHash[0] = invocation.getArgument(3);
|
||||
return false;
|
||||
});
|
||||
when(settlementOperations.lockByKey(anyLong(), anyString()))
|
||||
.thenAnswer(invocation -> {
|
||||
operation.setRequestHash(requestHash[0]);
|
||||
return operation;
|
||||
});
|
||||
|
||||
assertThat(service.settle(settlementRequest())).isEqualTo(expected);
|
||||
verify(locations, never()).releaseSelectedReservations(any());
|
||||
verify(locations, never()).transferToChildByReservation(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPdaSettlementWhenIdempotencyKeyHasDifferentRequest() {
|
||||
PalletLocationSettlementOperationEntity operation = new PalletLocationSettlementOperationEntity();
|
||||
operation.setOperationStatus("COMPLETED");
|
||||
operation.setRequestHash("different-request-hash");
|
||||
when(locations.lockOccupiedResources(0L, "firstleg-service", 88L)).thenReturn(sourceResources());
|
||||
when(settlementOperations.createIfAbsent(anyLong(), anyLong(), anyString(), anyString()))
|
||||
.thenReturn(false);
|
||||
when(settlementOperations.lockByKey(anyLong(), anyString()))
|
||||
.thenReturn(operation);
|
||||
|
||||
assertThatThrownBy(() -> service.settle(settlementRequest())).isInstanceOf(BizException.class);
|
||||
verify(locations, never()).releaseSelectedReservations(any());
|
||||
verify(locations, never()).transferToChildByReservation(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPdaSettlementFromNonFirstlegService() {
|
||||
PalletAllocationDtos.SettlementRequest invalid = new PalletAllocationDtos.SettlementRequest(
|
||||
0L, 20L, "other-service", 88L, 89L, 20, 12, "RK-001-S1",
|
||||
"PDA-SETTLEMENT-88-1", 9001L, "PDA settlement", List.of(101L),
|
||||
List.of(new PalletAllocationDtos.SettlementTransferItem(102L, 12)));
|
||||
|
||||
assertThatThrownBy(() -> service.settle(invalid)).isInstanceOf(BizException.class);
|
||||
verifyNoInteractions(settlementOperations);
|
||||
verify(locations, never()).lockOccupiedResources(anyLong(), anyString(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectPdaSettlementWhenPackageCountsDoNotBalance() {
|
||||
PalletAllocationDtos.SettlementRequest invalid = new PalletAllocationDtos.SettlementRequest(
|
||||
0L, 20L, "firstleg-service", 88L, 89L, 20, 11, "RK-001-S1",
|
||||
"PDA-SETTLEMENT-88-1", 9001L, "PDA settlement", List.of(101L),
|
||||
List.of(new PalletAllocationDtos.SettlementTransferItem(102L, 12)));
|
||||
PalletLocationSettlementOperationEntity operation = new PalletLocationSettlementOperationEntity();
|
||||
operation.setOperationStatus("PROCESSING");
|
||||
when(locations.lockOccupiedResources(0L, "firstleg-service", 88L)).thenReturn(sourceResources());
|
||||
when(settlementOperations.createIfAbsent(anyLong(), anyLong(), anyString(), anyString()))
|
||||
.thenAnswer(invocation -> {
|
||||
operation.setRequestHash(invocation.getArgument(3));
|
||||
return true;
|
||||
});
|
||||
when(settlementOperations.lockByKey(88L, "PDA-SETTLEMENT-88-1")).thenReturn(operation);
|
||||
|
||||
assertThatThrownBy(() -> service.settle(invalid)).isInstanceOf(BizException.class);
|
||||
verify(locations, never()).releaseSelectedReservations(any());
|
||||
verify(locations, never()).transferToChildByReservation(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectChangedChildForSamePdaSettlementKey() {
|
||||
PalletLocationSettlementOperationEntity operation = new PalletLocationSettlementOperationEntity();
|
||||
operation.setOperationStatus("COMPLETED");
|
||||
operation.setRequestHash("request-for-another-child");
|
||||
PalletAllocationDtos.SettlementRequest changedChild = new PalletAllocationDtos.SettlementRequest(
|
||||
0L, 20L, "firstleg-service", 88L, 90L, 20, 12, "RK-001-S2",
|
||||
"PDA-SETTLEMENT-88-1", 9001L, "PDA settlement", List.of(101L),
|
||||
List.of(new PalletAllocationDtos.SettlementTransferItem(102L, 12)));
|
||||
when(locations.lockOccupiedResources(0L, "firstleg-service", 88L)).thenReturn(sourceResources());
|
||||
when(settlementOperations.createIfAbsent(eq(88L), eq(90L), eq("PDA-SETTLEMENT-88-1"), anyString()))
|
||||
.thenReturn(false);
|
||||
when(settlementOperations.lockByKey(88L, "PDA-SETTLEMENT-88-1")).thenReturn(operation);
|
||||
|
||||
assertThatThrownBy(() -> service.settle(changedChild)).isInstanceOf(BizException.class);
|
||||
verify(settlementOperations).lockByKey(88L, "PDA-SETTLEMENT-88-1");
|
||||
verify(locations, never()).releaseSelectedReservations(any());
|
||||
verify(locations, never()).transferToChildByReservation(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTransferOccupiedResourceWithoutReleasingPallet() {
|
||||
PalletLocationTransferOperationEntity operation = new PalletLocationTransferOperationEntity();
|
||||
operation.setId(501L);
|
||||
operation.setOperationStatus("PROCESSING");
|
||||
when(transferOperations.createIfAbsent(anyLong(), anyString(), anyLong(), anyLong(), anyString(), anyString()))
|
||||
.thenAnswer(invocation -> {
|
||||
operation.setRequestHash(invocation.getArgument(5));
|
||||
return true;
|
||||
});
|
||||
when(transferOperations.lockByKey(anyLong(), anyString(), anyLong(), anyLong(), anyString()))
|
||||
.thenReturn(operation);
|
||||
when(locations.transferToChild(any())).thenReturn(List.of(new LocationDtos.TransferResult(
|
||||
101L, "LR101", 201L, 1, 20L, 30L, "TP-001", 40L, "A-01",
|
||||
new BigDecimal("1.200000"), 5, "OCCUPIED")));
|
||||
PalletEntity pallet = pallet(30L, "TP-001");
|
||||
pallet.setStatus("OCCUPIED");
|
||||
pallet.setCapacityCbm(new BigDecimal("1.500000"));
|
||||
when(pallets.findOwned(10001L, 20L, 30L)).thenReturn(pallet);
|
||||
|
||||
var result = service.transfer(new PalletAllocationDtos.TransferRequest(
|
||||
10001L, "FIRSTLEG", 88L, 89L, "RK-001-S1", "transfer-88-89", 9001L,
|
||||
List.of(new PalletAllocationDtos.TransferItem(40L, 5))));
|
||||
|
||||
assertThat(result).singleElement().satisfies(item -> {
|
||||
assertThat(item.occupancyId()).isEqualTo(201L);
|
||||
assertThat(item.palletCapacityCbm()).isEqualByComparingTo("1.500000");
|
||||
assertThat(item.remainingPackageCount()).isEqualTo(5);
|
||||
assertThat(item.state()).isEqualTo("OCCUPIED");
|
||||
});
|
||||
verify(pallets, never()).transitionStatus(10001L, 20L, 30L, List.of("OCCUPIED"), "AVAILABLE");
|
||||
verify(transferOperations).complete(org.mockito.ArgumentMatchers.eq(501L), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReplayCompletedTransferForSameIdempotencyKey() throws Exception {
|
||||
var expected = List.of(new PalletAllocationDtos.TransferResult(101L, "LR101", 201L, 1,
|
||||
0L, 20L, 30L, "TP-001", new BigDecimal("1.5"), 40L, "A-01",
|
||||
new BigDecimal("1.2"), 5, "OCCUPIED"));
|
||||
PalletLocationTransferOperationEntity operation = new PalletLocationTransferOperationEntity();
|
||||
operation.setOperationStatus("COMPLETED");
|
||||
operation.setResponseSnapshot(new ObjectMapper().writeValueAsString(expected));
|
||||
String[] requestHash = new String[1];
|
||||
when(transferOperations.createIfAbsent(anyLong(), anyString(), anyLong(), anyLong(), anyString(), anyString()))
|
||||
.thenAnswer(invocation -> {
|
||||
requestHash[0] = invocation.getArgument(5);
|
||||
return false;
|
||||
});
|
||||
when(transferOperations.lockByKey(anyLong(), anyString(), anyLong(), anyLong(), anyString()))
|
||||
.thenAnswer(invocation -> {
|
||||
operation.setRequestHash(requestHash[0]);
|
||||
return operation;
|
||||
});
|
||||
|
||||
assertThat(service.transfer(transferRequest())).isEqualTo(expected);
|
||||
verify(locations, never()).transferToChild(any());
|
||||
verify(transferOperations, never()).complete(anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectSameIdempotencyKeyWithDifferentRequestHash() {
|
||||
PalletLocationTransferOperationEntity operation = new PalletLocationTransferOperationEntity();
|
||||
operation.setOperationStatus("COMPLETED");
|
||||
operation.setRequestHash("different-request-hash");
|
||||
when(transferOperations.createIfAbsent(anyLong(), anyString(), anyLong(), anyLong(), anyString(), anyString()))
|
||||
.thenReturn(false);
|
||||
when(transferOperations.lockByKey(anyLong(), anyString(), anyLong(), anyLong(), anyString()))
|
||||
.thenReturn(operation);
|
||||
|
||||
assertThatThrownBy(() -> service.transfer(transferRequest())).isInstanceOf(BizException.class);
|
||||
verify(locations, never()).transferToChild(any());
|
||||
}
|
||||
|
||||
private static PalletAllocationDtos.TransferRequest transferRequest() {
|
||||
return new PalletAllocationDtos.TransferRequest(10001L, "FIRSTLEG", 88L, 89L, "RK-001-S1",
|
||||
"transfer-88-89", 9001L, List.of(new PalletAllocationDtos.TransferItem(40L, 5)));
|
||||
}
|
||||
|
||||
private static PalletAllocationDtos.SelectiveReleaseRequest selectiveReleaseRequest() {
|
||||
return new PalletAllocationDtos.SelectiveReleaseRequest(10001L, "FIRSTLEG", 88L,
|
||||
"pda-release-88-201", 9001L, "PDA full pick", List.of(40L));
|
||||
}
|
||||
|
||||
private static PalletAllocationDtos.SettlementRequest settlementRequest() {
|
||||
return new PalletAllocationDtos.SettlementRequest(0L, 20L, "firstleg-service", 88L, 89L, 20, 12,
|
||||
"RK-001-S1", "PDA-SETTLEMENT-88-1", 9001L, "PDA settlement", List.of(101L),
|
||||
List.of(new PalletAllocationDtos.SettlementTransferItem(102L, 12)));
|
||||
}
|
||||
|
||||
private static List<LocationDtos.OccupiedResource> sourceResources() {
|
||||
return List.of(
|
||||
new LocationDtos.OccupiedResource(101L, 201L, 1, 20L, 30L, 40L, "A-01", false, 8),
|
||||
new LocationDtos.OccupiedResource(102L, 202L, 2, 20L, 31L, 41L, "A-02", false, 12));
|
||||
}
|
||||
|
||||
private static PalletAllocationDtos.ReserveRequest reserve(long businessId) {
|
||||
|
||||
Reference in New Issue
Block a user