287 lines
12 KiB
Java
287 lines
12 KiB
Java
package com.baiyue.warehouse.service;
|
|
|
|
import com.baiyue.common.core.exception.BizException;
|
|
import com.baiyue.warehouse.dto.WarehouseResourceLockRequests;
|
|
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
|
|
import com.baiyue.warehouse.entity.WarehousePalletEntity;
|
|
import com.baiyue.warehouse.entity.WarehouseResourceLockEntity;
|
|
import com.baiyue.warehouse.repository.WarehouseResourceLockRepository;
|
|
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 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.Mockito.doAnswer;
|
|
import static org.mockito.Mockito.never;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
@ExtendWith(MockitoExtension.class)
|
|
class WarehouseResourceLockServiceTest {
|
|
private static final long TENANT = 101L;
|
|
private static final long WAREHOUSE = 201L;
|
|
private static final long PALLET = 301L;
|
|
private static final long LOCATION = 401L;
|
|
|
|
@Mock WarehouseResourceLockRepository repository;
|
|
private WarehouseResourceLockService service;
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
service = new WarehouseResourceLockService();
|
|
ReflectionTestUtils.setField(service, "repository", repository);
|
|
}
|
|
|
|
@Test
|
|
void atomicallyOccupiesIdlePalletAndLocation() {
|
|
var request = request("reserve-1");
|
|
when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(pallet("IDLE"));
|
|
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("IDLE"));
|
|
when(repository.occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION)).thenReturn(1);
|
|
when(repository.occupyLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(1);
|
|
doAnswer(call -> {
|
|
((WarehouseResourceLockEntity) call.getArgument(0)).setId(501L);
|
|
return null;
|
|
}).when(repository).insert(any());
|
|
|
|
var result = service.reserve(request);
|
|
|
|
assertThat(result.lockId()).isEqualTo(501L);
|
|
assertThat(result.status()).isEqualTo("ACTIVE");
|
|
ArgumentCaptor<WarehouseResourceLockEntity> lock = ArgumentCaptor.forClass(WarehouseResourceLockEntity.class);
|
|
verify(repository).insert(lock.capture());
|
|
assertThat(lock.getValue().getReservationKey()).isEqualTo("reserve-1");
|
|
assertThat(lock.getValue().getPalletId()).isEqualTo(PALLET);
|
|
assertThat(lock.getValue().getLocationId()).isEqualTo(LOCATION);
|
|
}
|
|
|
|
@Test
|
|
void duplicateReservationReturnsExistingLockWithoutMutatingResources() {
|
|
WarehouseResourceLockEntity existing = activeLock();
|
|
when(repository.findByReservationKey(TENANT, "reserve-1")).thenReturn(existing);
|
|
|
|
var result = service.reserve(request("reserve-1"));
|
|
|
|
assertThat(result.lockId()).isEqualTo(existing.getId());
|
|
verify(repository, never()).occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION);
|
|
verify(repository, never()).insert(any());
|
|
}
|
|
|
|
@Test
|
|
void concurrentDuplicateReturnsExistingAfterResourcesBecomeOccupied() {
|
|
WarehouseResourceLockEntity existing = activeLock();
|
|
when(repository.findByReservationKey(TENANT, "reserve-1")).thenReturn(null, existing);
|
|
when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(pallet("OCCUPIED"));
|
|
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("OCCUPIED"));
|
|
|
|
var result = service.reserve(request("reserve-1"));
|
|
|
|
assertThat(result.lockId()).isEqualTo(existing.getId());
|
|
verify(repository, never()).occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION);
|
|
}
|
|
|
|
@Test
|
|
void reservationKeyCannotBeReusedForDifferentResources() {
|
|
WarehouseResourceLockEntity existing = activeLock();
|
|
existing.setPalletId(999L);
|
|
when(repository.findByReservationKey(TENANT, "reserve-1")).thenReturn(existing);
|
|
|
|
assertThatThrownBy(() -> service.reserve(request("reserve-1")))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("different resources");
|
|
}
|
|
|
|
@Test
|
|
void rejectsResourceOutsideTenantWarehouse() {
|
|
when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(null);
|
|
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("IDLE"));
|
|
|
|
assertThatThrownBy(() -> service.reserve(request("reserve-1")))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("tenant warehouse");
|
|
verify(repository, never()).occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION);
|
|
}
|
|
|
|
@Test
|
|
void rejectsNonIdleResources() {
|
|
when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(pallet("OCCUPIED"));
|
|
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("IDLE"));
|
|
|
|
assertThatThrownBy(() -> service.reserve(request("reserve-1")))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("Only IDLE");
|
|
}
|
|
|
|
@Test
|
|
void releaseRestoresBothResourcesAndClosesLock() {
|
|
WarehouseResourceLockEntity lock = activeLock();
|
|
when(repository.find(TENANT, lock.getId())).thenReturn(lock);
|
|
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("OCCUPIED"));
|
|
when(repository.releasePallet(lock)).thenReturn(1);
|
|
when(repository.releaseLocation(lock)).thenReturn(1);
|
|
when(repository.close(lock, "RELEASED")).thenReturn(1);
|
|
|
|
var result = service.release(TENANT, lock.getId());
|
|
|
|
assertThat(result.status()).isEqualTo("RELEASED");
|
|
verify(repository).releasePallet(lock);
|
|
verify(repository).releaseLocation(lock);
|
|
}
|
|
|
|
@Test
|
|
void scrapDiscardsPalletAndReleasesLocation() {
|
|
WarehouseResourceLockEntity lock = activeLock();
|
|
when(repository.find(TENANT, lock.getId())).thenReturn(lock);
|
|
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("OCCUPIED"));
|
|
when(repository.scrapPallet(lock)).thenReturn(1);
|
|
when(repository.releaseLocation(lock)).thenReturn(1);
|
|
when(repository.close(lock, "SCRAPPED")).thenReturn(1);
|
|
|
|
var result = service.scrap(TENANT, lock.getId());
|
|
|
|
assertThat(result.status()).isEqualTo("SCRAPPED");
|
|
verify(repository).scrapPallet(lock);
|
|
verify(repository).releaseLocation(lock);
|
|
}
|
|
|
|
@Test
|
|
void concurrentLocationClaimFailsWholeOperation() {
|
|
when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(pallet("IDLE"));
|
|
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(location("IDLE"));
|
|
when(repository.occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION)).thenReturn(1);
|
|
when(repository.occupyLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(0);
|
|
|
|
assertThatThrownBy(() -> service.reserve(request("reserve-1")))
|
|
.isInstanceOf(BizException.class)
|
|
.hasMessageContaining("no longer IDLE");
|
|
verify(repository, never()).insert(any());
|
|
}
|
|
|
|
@Test
|
|
void virtualLocationIsNeverOccupiedOrReleased() {
|
|
WarehouseLocationEntity virtual = location("ACTIVE");
|
|
virtual.setLocationType("VIRTUAL");
|
|
when(repository.findPallet(TENANT, WAREHOUSE, PALLET)).thenReturn(pallet("IDLE"));
|
|
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(virtual);
|
|
when(repository.occupyPallet(TENANT, WAREHOUSE, PALLET, LOCATION)).thenReturn(1);
|
|
doAnswer(call -> {
|
|
((WarehouseResourceLockEntity) call.getArgument(0)).setId(502L);
|
|
return null;
|
|
}).when(repository).insert(any());
|
|
|
|
service.reserve(request("virtual-reserve"));
|
|
|
|
verify(repository, never()).occupyLocation(TENANT, WAREHOUSE, LOCATION);
|
|
}
|
|
|
|
@Test
|
|
void scrappingAtVirtualLocationKeepsLocationUntouched() {
|
|
WarehouseResourceLockEntity lock = activeLock();
|
|
WarehouseLocationEntity virtual = location("ACTIVE");
|
|
virtual.setLocationType("VIRTUAL");
|
|
when(repository.find(TENANT, lock.getId())).thenReturn(lock);
|
|
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(virtual);
|
|
when(repository.scrapPalletAtVirtualLocation(lock)).thenReturn(1);
|
|
when(repository.close(lock, "SCRAPPED")).thenReturn(1);
|
|
|
|
var result = service.scrap(TENANT, lock.getId());
|
|
|
|
assertThat(result.status()).isEqualTo("SCRAPPED");
|
|
verify(repository).scrapPalletAtVirtualLocation(lock);
|
|
verify(repository, never()).releaseLocation(lock);
|
|
}
|
|
|
|
@Test
|
|
void scrapByPalletClosesItsActiveLockAndKeepsLocationReference() {
|
|
WarehousePalletEntity pallet = pallet("OCCUPIED");
|
|
pallet.setWarehouseId(WAREHOUSE);
|
|
pallet.setLocationId(LOCATION);
|
|
WarehouseResourceLockEntity lock = activeLock();
|
|
when(repository.findPallet(TENANT, PALLET)).thenReturn(pallet);
|
|
when(repository.findActiveByPallet(TENANT, PALLET)).thenReturn(lock);
|
|
when(repository.find(TENANT, lock.getId())).thenReturn(lock);
|
|
WarehouseLocationEntity virtual = location("ACTIVE");
|
|
virtual.setLocationType("VIRTUAL");
|
|
when(repository.findLocation(TENANT, WAREHOUSE, LOCATION)).thenReturn(virtual);
|
|
when(repository.scrapPalletAtVirtualLocation(lock)).thenReturn(1);
|
|
when(repository.close(lock, "SCRAPPED")).thenReturn(1);
|
|
|
|
var result = service.scrapByPallet(TENANT, PALLET);
|
|
|
|
assertThat(result.status()).isEqualTo("SCRAPPED");
|
|
assertThat(result.locationId()).isEqualTo(LOCATION);
|
|
verify(repository).scrapPalletAtVirtualLocation(lock);
|
|
}
|
|
|
|
@Test
|
|
void sharedDropshipPalletIsScrappedThroughItsActualLockOwner() {
|
|
WarehousePalletEntity pallet = pallet("OCCUPIED");
|
|
pallet.setPalletCode("DP-260910-0001");
|
|
pallet.setWarehouseId(WAREHOUSE);
|
|
pallet.setLocationId(LOCATION);
|
|
WarehouseResourceLockEntity lock = activeLock();
|
|
lock.setCustomerId(TENANT + 1);
|
|
when(repository.findPallet(TENANT, PALLET)).thenReturn(pallet);
|
|
when(repository.findAnyActiveByPallet(PALLET)).thenReturn(lock);
|
|
when(repository.find(TENANT + 1, lock.getId())).thenReturn(lock);
|
|
WarehouseLocationEntity virtual = location("ACTIVE");
|
|
virtual.setLocationType("VIRTUAL");
|
|
when(repository.findLocation(TENANT + 1, WAREHOUSE, LOCATION)).thenReturn(virtual);
|
|
when(repository.scrapPalletAtVirtualLocation(lock)).thenReturn(1);
|
|
when(repository.close(lock, "SCRAPPED")).thenReturn(1);
|
|
|
|
assertThat(service.scrapByPallet(TENANT, PALLET).status()).isEqualTo("SCRAPPED");
|
|
|
|
verify(repository).findAnyActiveByPallet(PALLET);
|
|
verify(repository).find(TENANT + 1, lock.getId());
|
|
}
|
|
|
|
@Test
|
|
void scrapByPalletIsIdempotentWhenAlreadyScrapped() {
|
|
WarehousePalletEntity pallet = pallet("SCRAPPED");
|
|
pallet.setWarehouseId(WAREHOUSE);
|
|
pallet.setLocationId(LOCATION);
|
|
when(repository.findPallet(TENANT, PALLET)).thenReturn(pallet);
|
|
|
|
assertThat(service.scrapByPallet(TENANT, PALLET).status()).isEqualTo("SCRAPPED");
|
|
verify(repository, never()).findActiveByPallet(TENANT, PALLET);
|
|
}
|
|
|
|
private static WarehouseResourceLockRequests.ReserveRequest request(String key) {
|
|
return new WarehouseResourceLockRequests.ReserveRequest(TENANT, WAREHOUSE, PALLET, LOCATION,
|
|
"INBOUND_PLAN_PALLET", "plan-pallet-1", key);
|
|
}
|
|
|
|
private static WarehousePalletEntity pallet(String status) {
|
|
WarehousePalletEntity entity = new WarehousePalletEntity();
|
|
entity.setStatus(status);
|
|
return entity;
|
|
}
|
|
|
|
private static WarehouseLocationEntity location(String status) {
|
|
WarehouseLocationEntity entity = new WarehouseLocationEntity();
|
|
entity.setStatus(status);
|
|
return entity;
|
|
}
|
|
|
|
private static WarehouseResourceLockEntity activeLock() {
|
|
WarehouseResourceLockEntity lock = new WarehouseResourceLockEntity();
|
|
lock.setId(501L);
|
|
lock.setCustomerId(TENANT);
|
|
lock.setWarehouseId(WAREHOUSE);
|
|
lock.setPalletId(PALLET);
|
|
lock.setLocationId(LOCATION);
|
|
lock.setBusinessType("INBOUND_PLAN_PALLET");
|
|
lock.setBusinessId("plan-pallet-1");
|
|
lock.setReservationKey("reserve-1");
|
|
lock.setStatus("ACTIVE");
|
|
return lock;
|
|
}
|
|
}
|