package com.baiyue.warehouse.service; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baiyue.common.core.exception.BizException; import com.baiyue.warehouse.dto.LocationRequests; import com.baiyue.warehouse.dto.PalletRequests; import com.baiyue.warehouse.entity.WarehouseLocationEntity; import com.baiyue.warehouse.entity.WarehousePalletEntity; import com.baiyue.warehouse.dto.SharedWarehouseReference; import com.baiyue.warehouse.repository.WarehouseResourceRepository; 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 java.math.BigDecimal; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; 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 WarehouseResourceServiceTest { private static final long TENANT = 10001L; private static final long WAREHOUSE = 20001L; @Mock private WarehouseResourceRepository repository; @Mock private DomesticWarehouseDirectory warehouseDirectory; private WarehouseResourceService service; @BeforeEach void setUp() { service = new WarehouseResourceService(); ReflectionTestUtils.setField(service, "repository", repository); ReflectionTestUtils.setField(service, "warehouseDirectory", warehouseDirectory); org.mockito.Mockito.lenient().when(warehouseDirectory.required(WAREHOUSE)) .thenReturn(warehouse()); } @Test void resourceIdsUseMybatisSnowflakeAssignment() throws NoSuchFieldException { assertThat(WarehouseLocationEntity.class.getDeclaredField("id").getAnnotation(TableId.class).type()) .isEqualTo(IdType.ASSIGN_ID); assertThat(WarehousePalletEntity.class.getDeclaredField("id").getAnnotation(TableId.class).type()) .isEqualTo(IdType.ASSIGN_ID); } @Test void resolvesOnlyPublicLocationInsideBoundWarehouse() { WarehouseLocationEntity location = new WarehouseLocationEntity(); location.setId(30001L); location.setWarehouseId(WAREHOUSE); location.setCustomerId(0L); location.setLocationCode("A-01"); location.setLocationType("STORAGE"); location.setMaxLoadKg(new BigDecimal("1000")); location.setStatus("IDLE"); when(repository.findLocationByCode(0L, WAREHOUSE, "A-01")).thenReturn(location); var result = service.resolvePublicLocation(WAREHOUSE, " A-01 "); assertThat(result.locationId()).isEqualTo(30001L); assertThat(result.locationCode()).isEqualTo("A-01"); } @Test void createsLocationWithSnowflakeAssignedByMapper() { doAnswer(invocation -> { ((WarehouseLocationEntity) invocation.getArgument(0)).setId(2089000000000000001L); return null; }).when(repository).insertLocation(org.mockito.ArgumentMatchers.any()); Long id = service.createLocation(TENANT, WAREHOUSE, locationRequest("A-01")); assertThat(id).isEqualTo(2089000000000000001L); ArgumentCaptor captor = ArgumentCaptor.forClass(WarehouseLocationEntity.class); verify(repository).insertLocation(captor.capture()); assertThat(captor.getValue().getCustomerId()).isEqualTo(TENANT); assertThat(captor.getValue().getWarehouseId()).isEqualTo(WAREHOUSE); } @Test void platformPalletUsesPublicScopeAgainstAnyExistingWarehouse() { WarehouseLocationEntity location = new WarehouseLocationEntity(); location.setId(30001L); when(repository.findLocation(0L, WAREHOUSE, 30001L)).thenReturn(location); service.createPallet(0L, WAREHOUSE, new PalletRequests.SaveRequest( "PLT-PUBLIC", "STANDARD", null, BigDecimal.ONE, "IDLE", 30001L, null)); ArgumentCaptor captor = ArgumentCaptor.forClass(WarehousePalletEntity.class); verify(repository).insertPallet(captor.capture()); assertThat(captor.getValue().getCustomerId()).isZero(); assertThat(captor.getValue().getWarehouseId()).isEqualTo(WAREHOUSE); } @Test void rejectsResourceAccessWhenWarehouseDoesNotBelongToTenant() { when(warehouseDirectory.required(WAREHOUSE)).thenThrow(new BizException( com.baiyue.common.core.api.ResultCode.NOT_FOUND, "仓库不存在")); assertThatThrownBy(() -> service.createLocation(TENANT, WAREHOUSE, locationRequest("A-01"))) .isInstanceOf(BizException.class) .hasMessageContaining("仓库不存在"); verify(repository, never()).insertLocation(org.mockito.ArgumentMatchers.any()); } @Test void rejectsDuplicateLocationCodeWithinWarehouse() { when(repository.locationCodeExists(TENANT, WAREHOUSE, "A-01", null)).thenReturn(true); assertThatThrownBy(() -> service.createLocation(TENANT, WAREHOUSE, locationRequest("A-01"))) .isInstanceOf(BizException.class) .hasMessageContaining("库位编码已存在"); } @Test void palletDefaultsToTwoCubicMetresAndBindsOwnedLocation() { WarehouseLocationEntity location = new WarehouseLocationEntity(); location.setId(30001L); when(repository.findLocation(TENANT, WAREHOUSE, 30001L)).thenReturn(location); doAnswer(invocation -> { ((WarehousePalletEntity) invocation.getArgument(0)).setId(2089000000000000002L); return null; }).when(repository).insertPallet(org.mockito.ArgumentMatchers.any()); Long id = service.createPallet(TENANT, WAREHOUSE, new PalletRequests.SaveRequest("PLT-01", "STANDARD", null, new BigDecimal("12.50"), "IDLE", 30001L, null)); assertThat(id).isEqualTo(2089000000000000002L); ArgumentCaptor captor = ArgumentCaptor.forClass(WarehousePalletEntity.class); verify(repository).insertPallet(captor.capture()); assertThat(captor.getValue().getCapacityCbm()).isEqualByComparingTo("2.0000"); assertThat(captor.getValue().getLocationId()).isEqualTo(30001L); } @Test void rejectsLocationFromAnotherWarehouseWhenBindingPallet() { when(repository.findLocation(TENANT, WAREHOUSE, 30002L)).thenReturn(null); assertThatThrownBy(() -> service.createPallet(TENANT, WAREHOUSE, new PalletRequests.SaveRequest("PLT-01", "STANDARD", null, BigDecimal.ONE, "IDLE", 30002L, null))) .isInstanceOf(BizException.class) .hasMessageContaining("库位不存在"); verify(repository, never()).insertPallet(org.mockito.ArgumentMatchers.any()); } @Test void preventsDeletingLocationThatStillHasPallets() { when(repository.findLocation(TENANT, WAREHOUSE, 30001L)).thenReturn(new WarehouseLocationEntity()); when(repository.locationHasPallets(TENANT, WAREHOUSE, 30001L)).thenReturn(true); assertThatThrownBy(() -> service.deleteLocation(TENANT, WAREHOUSE, 30001L)) .isInstanceOf(BizException.class) .hasMessageContaining("仍绑定托盘"); verify(repository, never()).deleteLocation(30001L); } @Test void preventsDeletingOccupiedPallet() { WarehousePalletEntity pallet = new WarehousePalletEntity(); pallet.setStatus("OCCUPIED"); when(repository.findPallet(TENANT, WAREHOUSE, 40001L)).thenReturn(pallet); assertThatThrownBy(() -> service.deletePallet(TENANT, WAREHOUSE, 40001L)) .isInstanceOf(BizException.class) .hasMessageContaining("占用中的托盘"); verify(repository, never()).deletePallet(40001L); } @Test void preventsDeletingScrappedPalletSoItsIdentityCannotBeReused() { WarehousePalletEntity pallet = new WarehousePalletEntity(); pallet.setStatus("SCRAPPED"); when(repository.findPallet(TENANT, WAREHOUSE, 40003L)).thenReturn(pallet); assertThatThrownBy(() -> service.deletePallet(TENANT, WAREHOUSE, 40003L)) .isInstanceOf(BizException.class) .hasMessageContaining("cannot be reused"); verify(repository, never()).deletePallet(40003L); } @Test void rejectsUnknownPalletStatus() { assertThatThrownBy(() -> service.createPallet(TENANT, WAREHOUSE, new PalletRequests.SaveRequest("PLT-01", "STANDARD", null, BigDecimal.ONE, "BROKEN", null, null))) .isInstanceOf(BizException.class) .hasMessageContaining("IDLE、OCCUPIED、SCRAPPED"); } private static LocationRequests.SaveRequest locationRequest(String code) { return new LocationRequests.SaveRequest("A", code, "STORAGE", new BigDecimal("1000.000"), "ACTIVE", null); } @Test void generatesRestartSafePalletCodeAndUsesDefaultVirtualLocation() { WarehouseLocationEntity location = new WarehouseLocationEntity(); location.setId(30003L); location.setLocationType("VIRTUAL"); location.setStatus("ACTIVE"); when(repository.findLocationByCode(TENANT, WAREHOUSE, "LOC-US-LAX-01-DEFAULT")) .thenReturn(location); when(repository.findLocation(TENANT, WAREHOUSE, 30003L)).thenReturn(location); service.createPallet(TENANT, WAREHOUSE, new PalletRequests.SaveRequest(null, "STANDARD", null, BigDecimal.ONE, "IDLE", null, null)); ArgumentCaptor captor = ArgumentCaptor.forClass(WarehousePalletEntity.class); verify(repository).insertPallet(captor.capture()); assertThat(captor.getValue().getPalletCode()).matches("PLT-US-LAX-01-[0-9]{8}-[0-9]+"); assertThat(captor.getValue().getLocationId()).isEqualTo(30003L); assertThat(captor.getValue().getId()).isNotNull(); } @Test void defaultVirtualLocationIsInsertedIdempotentlyForAnExistingWarehouse() { WarehouseLocationEntity resolved = new WarehouseLocationEntity(); resolved.setId(30004L); resolved.setLocationType("VIRTUAL"); resolved.setStatus("ACTIVE"); when(repository.findLocationByCode(TENANT, WAREHOUSE, "LOC-US-LAX-01-DEFAULT")) .thenReturn(null, resolved); WarehouseLocationEntity result = service.ensureDefaultVirtualLocation(TENANT, WAREHOUSE); assertThat(result.getId()).isEqualTo(30004L); ArgumentCaptor location = ArgumentCaptor.forClass(WarehouseLocationEntity.class); verify(repository).insertDefaultVirtualLocationIgnore(location.capture()); assertThat(location.getValue().getLocationCode()).isEqualTo("LOC-US-LAX-01-DEFAULT"); assertThat(location.getValue().getLocationType()).isEqualTo("VIRTUAL"); assertThat(location.getValue().getStatus()).isEqualTo("ACTIVE"); } @Test void rejectsAConflictingDefaultLocationCode() { WarehouseLocationEntity conflict = new WarehouseLocationEntity(); conflict.setLocationType("STORAGE"); conflict.setStatus("ACTIVE"); when(repository.findLocationByCode(TENANT, WAREHOUSE, "LOC-US-LAX-01-DEFAULT")) .thenReturn(conflict); assertThatThrownBy(() -> service.ensureDefaultVirtualLocation(TENANT, WAREHOUSE)) .isInstanceOf(BizException.class) .hasMessageContaining("Default virtual location code"); verify(repository, never()).insertDefaultVirtualLocationIgnore(org.mockito.ArgumentMatchers.any()); } @Test void scrappedPalletCannotBeReactivated() { WarehousePalletEntity existing = new WarehousePalletEntity(); existing.setId(40002L); existing.setPalletCode("PLT-OLD"); existing.setStatus("SCRAPPED"); existing.setVersion(0); when(repository.findPallet(TENANT, WAREHOUSE, 40002L)).thenReturn(existing); assertThatThrownBy(() -> service.updatePallet(TENANT, WAREHOUSE, 40002L, new PalletRequests.SaveRequest(null, "STANDARD", null, BigDecimal.ONE, "IDLE", null, null, null, 0))) .isInstanceOf(BizException.class) .hasMessageContaining("cannot be reused"); } @Test void updatePalletChangesCodeAndFbaWarehouseCode() { WarehousePalletEntity existing = new WarehousePalletEntity(); existing.setId(40003L); existing.setPalletCode("PLT-OLD"); existing.setStatus("IDLE"); existing.setVersion(2); when(repository.findPallet(TENANT, WAREHOUSE, 40003L)).thenReturn(existing); when(repository.updatePallet(org.mockito.ArgumentMatchers.any())).thenReturn(1); service.updatePallet(TENANT, WAREHOUSE, 40003L, new PalletRequests.SaveRequest("PLT-NEW", "WOOD", new BigDecimal("2.5000"), new BigDecimal("25.00"), "IDLE", null, "updated", "ONT8", 2)); ArgumentCaptor captor = ArgumentCaptor.forClass(WarehousePalletEntity.class); verify(repository).palletCodeExists(TENANT, WAREHOUSE, "PLT-NEW", 40003L); verify(repository).updatePallet(captor.capture()); assertThat(captor.getValue().getPalletCode()).isEqualTo("PLT-NEW"); assertThat(captor.getValue().getFbaWarehouseCode()).isEqualTo("ONT8"); assertThat(captor.getValue().getVersion()).isEqualTo(2); } @Test void updatePalletRejectsDuplicateCodeInSameWarehouse() { WarehousePalletEntity existing = new WarehousePalletEntity(); existing.setId(40004L); existing.setPalletCode("PLT-OLD"); existing.setStatus("IDLE"); existing.setVersion(0); when(repository.findPallet(TENANT, WAREHOUSE, 40004L)).thenReturn(existing); when(repository.palletCodeExists(TENANT, WAREHOUSE, "PLT-DUP", 40004L)).thenReturn(true); assertThatThrownBy(() -> service.updatePallet(TENANT, WAREHOUSE, 40004L, new PalletRequests.SaveRequest("PLT-DUP", "WOOD", BigDecimal.ONE, BigDecimal.ONE, "IDLE", null, null, "ONT8", 0))) .isInstanceOf(BizException.class) .hasMessageContaining("托盘编码已存在"); verify(repository, never()).updatePallet(org.mockito.ArgumentMatchers.any()); } @Test void updatePalletRejectsStaleVersionBeforeMutation() { WarehousePalletEntity existing = new WarehousePalletEntity(); existing.setId(40005L); existing.setPalletCode("PLT-OLD"); existing.setStatus("IDLE"); existing.setVersion(3); when(repository.findPallet(TENANT, WAREHOUSE, 40005L)).thenReturn(existing); assertThatThrownBy(() -> service.updatePallet(TENANT, WAREHOUSE, 40005L, new PalletRequests.SaveRequest("PLT-STALE", "WOOD", BigDecimal.ONE, BigDecimal.ONE, "IDLE", null, null, "ONT8", 2))) .isInstanceOf(BizException.class) .hasMessageContaining("版本已变化"); verify(repository, never()).palletCodeExists(org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.any()); verify(repository, never()).updatePallet(org.mockito.ArgumentMatchers.any()); } private static SharedWarehouseReference warehouse() { return new SharedWarehouseReference(WAREHOUSE, "US-LAX-01", "Los Angeles", "ACTIVE"); } }