fix(warehouse-overseas): enforce pallet location invariant
This commit is contained in:
+13
-2
@@ -15,7 +15,7 @@ class WarehousePalletTransitionMapperContractTest {
|
||||
@Test
|
||||
void annotationSqlUsesNativeComparisonOperatorsInsteadOfXmlEntities() throws Exception {
|
||||
Method locationQuery = WarehousePalletTransitionMapper.class.getMethod(
|
||||
"countActiveLocation", long.class, long.class);
|
||||
"lockPublicLocation", long.class, long.class);
|
||||
String selectSql = String.join(" ", locationQuery.getAnnotation(Select.class).value());
|
||||
|
||||
Method transition = Arrays.stream(WarehousePalletTransitionMapper.class.getMethods())
|
||||
@@ -24,9 +24,20 @@ class WarehousePalletTransitionMapperContractTest {
|
||||
.orElseThrow();
|
||||
String updateSql = String.join(" ", transition.getAnnotation(Update.class).value());
|
||||
|
||||
assertTrue(selectSql.contains("status<>'INACTIVE'"));
|
||||
assertTrue(selectSql.contains("FOR UPDATE"));
|
||||
assertTrue(updateSql.contains("status<>'SCRAPPED'"));
|
||||
assertFalse(selectSql.contains("<") || selectSql.contains(">"));
|
||||
assertFalse(updateSql.contains("<") || updateSql.contains(">"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalResourceReleaseKeepsPalletBoundToTheNowIdleLocation() throws Exception {
|
||||
Method release = WarehouseResourceLockMapper.class.getMethod("releasePallet",
|
||||
long.class, long.class, long.class, long.class);
|
||||
String sql = String.join(" ", release.getAnnotation(Update.class).value());
|
||||
|
||||
assertTrue(sql.contains("SET status = 'IDLE'"));
|
||||
assertFalse(sql.contains("location_id = NULL"));
|
||||
assertTrue(sql.contains("location_id = #{locationId}"));
|
||||
}
|
||||
}
|
||||
|
||||
+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