fix(warehouse-overseas): enforce pallet location invariant

This commit is contained in:
2026-09-08 14:47:35 +08:00
parent 07688005a1
commit a0ad3382af
9 changed files with 276 additions and 25 deletions
@@ -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");
@@ -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;
}
}