fix(warehouse-overseas): enforce pallet location invariant
This commit is contained in:
+76
-2
@@ -2,6 +2,7 @@ package com.baiyue.warehouse.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.warehouse.dto.PalletTransitionRequests;
|
||||
import com.baiyue.warehouse.entity.WarehouseLocationEntity;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletEntity;
|
||||
import com.baiyue.warehouse.entity.WarehousePalletOperationEntity;
|
||||
import com.baiyue.warehouse.mapper.WarehousePalletTransitionMapper;
|
||||
@@ -30,7 +31,9 @@ class WarehousePalletTransitionServiceTest {
|
||||
@Test void appliesVersionedPublicPalletTransitionAndPersistsCompensationReceipt() {
|
||||
WarehousePalletEntity pallet = pallet("P-1", "IDLE", 4);
|
||||
when(mapper.lockPublicPallet(88L, 10L)).thenReturn(pallet);
|
||||
when(mapper.countActiveLocation(88L, 20L)).thenReturn(1L);
|
||||
when(mapper.lockPublicLocation(88L, 11L)).thenReturn(location(11L, "STORAGE", "IDLE"));
|
||||
when(mapper.lockPublicLocation(88L, 20L)).thenReturn(location(20L, "STORAGE", "IDLE"));
|
||||
when(mapper.transitionLocation(88L, 20L, "IDLE", "OCCUPIED")).thenReturn(1);
|
||||
when(mapper.transition(88L, 10L, "OCCUPIED", 20L, 4)).thenReturn(1);
|
||||
|
||||
var response = service.apply(10L, new PalletTransitionRequests.Apply(
|
||||
@@ -56,9 +59,26 @@ class WarehousePalletTransitionServiceTest {
|
||||
verify(mapper, never()).transition(anyLong(), anyLong(), anyString(), anyLong(), anyInt());
|
||||
}
|
||||
|
||||
@Test void concurrentExactReplayAfterPalletLockDoesNotMutatePhysicalStateTwice() {
|
||||
WarehousePalletOperationEntity existing = operation();
|
||||
when(mapper.lockPublicPallet(88L, 10L)).thenReturn(pallet("P-1", "OCCUPIED", 5, 20L));
|
||||
when(mapper.lockOperation("operation-1")).thenReturn(existing);
|
||||
|
||||
var response = service.apply(10L, new PalletTransitionRequests.Apply(
|
||||
88L, 4, "OCCUPIED", 20L, "operation-1", "assemble"));
|
||||
|
||||
assertTrue(response.replayed());
|
||||
verify(mapper, never()).transition(anyLong(), anyLong(), anyString(), anyLong(), anyInt());
|
||||
verify(mapper, never()).transitionLocation(anyLong(), anyLong(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test void compensationRequiresExactAppliedVersionAndRestoresBeforeState() {
|
||||
WarehousePalletOperationEntity existing = operation();
|
||||
when(mapper.lockOperation("operation-1")).thenReturn(existing);
|
||||
when(mapper.lockPublicPallet(88L, 10L)).thenReturn(pallet("P-1", "OCCUPIED", 5, 20L));
|
||||
when(mapper.lockPublicLocation(88L, 11L)).thenReturn(location(11L, "STORAGE", "IDLE"));
|
||||
when(mapper.lockPublicLocation(88L, 20L)).thenReturn(location(20L, "STORAGE", "OCCUPIED"));
|
||||
when(mapper.transitionLocation(88L, 20L, "OCCUPIED", "IDLE")).thenReturn(1);
|
||||
when(mapper.transition(88L, 10L, "IDLE", 11L, 5)).thenReturn(1);
|
||||
|
||||
var response = service.compensate(10L, "operation-1", new PalletTransitionRequests.Compensate(88L));
|
||||
@@ -67,6 +87,53 @@ class WarehousePalletTransitionServiceTest {
|
||||
verify(mapper).updateById(existing);
|
||||
}
|
||||
|
||||
@Test void unbindAtomicallyReturnsRealLocationToIdleWhileKeepingAssociation() {
|
||||
when(mapper.lockPublicPallet(88L, 10L)).thenReturn(pallet("P-1", "OCCUPIED", 4));
|
||||
when(mapper.lockPublicLocation(88L, 11L)).thenReturn(location(11L, "STORAGE", "OCCUPIED"));
|
||||
when(mapper.transitionLocation(88L, 11L, "OCCUPIED", "IDLE")).thenReturn(1);
|
||||
when(mapper.transition(88L, 10L, "IDLE", 11L, 4)).thenReturn(1);
|
||||
|
||||
service.apply(10L, new PalletTransitionRequests.Apply(
|
||||
88L, 4, "IDLE", 11L, "unbind-1", "unbind"));
|
||||
|
||||
verify(mapper).transitionLocation(88L, 11L, "OCCUPIED", "IDLE");
|
||||
verify(mapper).transition(88L, 10L, "IDLE", 11L, 4);
|
||||
}
|
||||
|
||||
@Test void virtualLocationRemainsActiveAcrossPalletStatusChanges() {
|
||||
when(mapper.lockPublicPallet(88L, 10L)).thenReturn(pallet("P-1", "OCCUPIED", 4));
|
||||
when(mapper.lockPublicLocation(88L, 11L)).thenReturn(location(11L, "VIRTUAL", "ACTIVE"));
|
||||
when(mapper.transition(88L, 10L, "IDLE", 11L, 4)).thenReturn(1);
|
||||
|
||||
service.apply(10L, new PalletTransitionRequests.Apply(
|
||||
88L, 4, "IDLE", 11L, "virtual-unbind", null));
|
||||
|
||||
verify(mapper, never()).transitionLocation(anyLong(), anyLong(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test void inconsistentIdlePalletAndOccupiedLocationIsRejectedBeforeMutation() {
|
||||
when(mapper.lockPublicPallet(88L, 10L)).thenReturn(pallet("P-1", "IDLE", 4));
|
||||
when(mapper.lockPublicLocation(88L, 11L)).thenReturn(location(11L, "STORAGE", "OCCUPIED"));
|
||||
|
||||
assertThrows(BizException.class, () -> service.apply(10L, new PalletTransitionRequests.Apply(
|
||||
88L, 4, "OCCUPIED", 11L, "broken-state", null)));
|
||||
|
||||
verify(mapper, never()).transition(anyLong(), anyLong(), anyString(), anyLong(), anyInt());
|
||||
verify(mapper, never()).transitionLocation(anyLong(), anyLong(), anyString(), anyString());
|
||||
}
|
||||
|
||||
@Test void locationConflictStopsPalletMutationSoTransactionCanRollBackCleanly() {
|
||||
when(mapper.lockPublicPallet(88L, 10L)).thenReturn(pallet("P-1", "OCCUPIED", 4));
|
||||
when(mapper.lockPublicLocation(88L, 11L)).thenReturn(location(11L, "STORAGE", "OCCUPIED"));
|
||||
when(mapper.transitionLocation(88L, 11L, "OCCUPIED", "IDLE")).thenReturn(0);
|
||||
|
||||
assertThrows(BizException.class, () -> service.apply(10L, new PalletTransitionRequests.Apply(
|
||||
88L, 4, "IDLE", 11L, "location-race", null)));
|
||||
|
||||
verify(mapper, never()).transition(anyLong(), anyLong(), anyString(), anyLong(), anyInt());
|
||||
verify(mapper, never()).insert(any(WarehousePalletOperationEntity.class));
|
||||
}
|
||||
|
||||
@Test void staleVersionIsRejectedWithoutPhysicalMutation() {
|
||||
when(mapper.lockPublicPallet(88L, 10L)).thenReturn(pallet("P-1", "IDLE", 5));
|
||||
assertThrows(BizException.class, () -> service.apply(10L, new PalletTransitionRequests.Apply(
|
||||
@@ -75,10 +142,17 @@ class WarehousePalletTransitionServiceTest {
|
||||
}
|
||||
|
||||
private static WarehousePalletEntity pallet(String code, String status, int version) {
|
||||
return pallet(code, status, version, 11L);
|
||||
}
|
||||
private static WarehousePalletEntity pallet(String code, String status, int version, long locationId) {
|
||||
WarehousePalletEntity value = new WarehousePalletEntity(); value.setId(10L); value.setCustomerId(0L);
|
||||
value.setWarehouseId(88L); value.setLocationId(11L); value.setPalletCode(code);
|
||||
value.setWarehouseId(88L); value.setLocationId(locationId); value.setPalletCode(code);
|
||||
value.setStatus(status); value.setVersion(version); return value;
|
||||
}
|
||||
private static WarehouseLocationEntity location(long id, String type, String status) {
|
||||
WarehouseLocationEntity value = new WarehouseLocationEntity(); value.setId(id); value.setCustomerId(0L);
|
||||
value.setWarehouseId(88L); value.setLocationType(type); value.setStatus(status); return value;
|
||||
}
|
||||
private static WarehousePalletOperationEntity operation() {
|
||||
WarehousePalletOperationEntity value = new WarehousePalletOperationEntity(); value.setId(1L);
|
||||
value.setOperationKey("operation-1");
|
||||
|
||||
+40
@@ -18,6 +18,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -68,6 +69,34 @@ class WarehouseResourceServiceTest {
|
||||
assertThat(result.locationCode()).isEqualTo("A-01");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvePalletExplicitlyReportsReservableRealLocation() {
|
||||
WarehousePalletEntity pallet = pallet(40001L, "IDLE", 30001L);
|
||||
WarehouseLocationEntity location = location(30001L, "STORAGE", "IDLE");
|
||||
when(repository.findPublicPalletsByCode("PLT-01")).thenReturn(List.of(pallet));
|
||||
when(repository.findLocation(0L, WAREHOUSE, 30001L)).thenReturn(location);
|
||||
|
||||
var result = service.resolvePublicPallet(" PLT-01 ");
|
||||
|
||||
assertThat(result.reservable()).isTrue();
|
||||
assertThat(result.locationType()).isEqualTo("STORAGE");
|
||||
assertThat(result.locationStatus()).isEqualTo("IDLE");
|
||||
assertThat(result.reservationBlockReason()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvePalletDoesNotAdvertiseBrokenIdlePalletAsReservable() {
|
||||
WarehousePalletEntity pallet = pallet(40001L, "IDLE", 30001L);
|
||||
WarehouseLocationEntity location = location(30001L, "STORAGE", "OCCUPIED");
|
||||
when(repository.findPublicPalletsByCode("PLT-01")).thenReturn(List.of(pallet));
|
||||
when(repository.findLocation(0L, WAREHOUSE, 30001L)).thenReturn(location);
|
||||
|
||||
var result = service.resolvePublicPallet("PLT-01");
|
||||
|
||||
assertThat(result.reservable()).isFalse();
|
||||
assertThat(result.reservationBlockReason()).isEqualTo("LOCATION_NOT_IDLE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsLocationWithSnowflakeAssignedByMapper() {
|
||||
doAnswer(invocation -> {
|
||||
@@ -333,4 +362,15 @@ class WarehouseResourceServiceTest {
|
||||
private static SharedWarehouseReference warehouse() {
|
||||
return new SharedWarehouseReference(WAREHOUSE, "US-LAX-01", "Los Angeles", "ACTIVE");
|
||||
}
|
||||
|
||||
private static WarehousePalletEntity pallet(long id, String status, long locationId) {
|
||||
WarehousePalletEntity value = new WarehousePalletEntity(); value.setId(id); value.setCustomerId(0L);
|
||||
value.setWarehouseId(WAREHOUSE); value.setLocationId(locationId); value.setPalletCode("PLT-01");
|
||||
value.setStatus(status); value.setVersion(3); value.setUnitPrice(BigDecimal.ONE); return value;
|
||||
}
|
||||
|
||||
private static WarehouseLocationEntity location(long id, String type, String status) {
|
||||
WarehouseLocationEntity value = new WarehouseLocationEntity(); value.setId(id); value.setCustomerId(0L);
|
||||
value.setWarehouseId(WAREHOUSE); value.setLocationType(type); value.setStatus(status); return value;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user