mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 12:09:35 +01:00
fix(inventory): Review-Fixes für US-6.1 InventoryCount
- GlobalExceptionHandler und ErrorResponse nach infrastructure.shared extrahieren (war fälschlich in usermanagement) - CountItem.deviation() prüft UOM-Kompatibilität - InvalidInventoryCountId Error-Typ für null/blank ID (400 statt 404) - saveChildren() auf UPSERT (UPDATE→INSERT) mit Orphan-Cleanup umstellen - logger.trace → logger.warn bei DB-Fehlern - Stocks ohne Batches in CreateInventoryCount überspringen - AuthorizationPort Defense in Depth in alle 3 InventoryCount Use Cases - Kombinierter DB-Index auf (storage_location_id, status)
This commit is contained in:
parent
c047ca93de
commit
a214002fab
19 changed files with 205 additions and 130 deletions
|
|
@ -8,6 +8,8 @@ import de.effigenix.shared.common.RepositoryError;
|
|||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.common.UnitOfMeasure;
|
||||
import de.effigenix.shared.persistence.UnitOfWork;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import de.effigenix.shared.security.AuthorizationPort;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -31,12 +33,15 @@ class CreateInventoryCountTest {
|
|||
@Mock private InventoryCountRepository inventoryCountRepository;
|
||||
@Mock private StockRepository stockRepository;
|
||||
@Mock private UnitOfWork unitOfWork;
|
||||
@Mock private AuthorizationPort authPort;
|
||||
|
||||
private CreateInventoryCount createInventoryCount;
|
||||
private final ActorId actorId = ActorId.of("user-1");
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
createInventoryCount = new CreateInventoryCount(inventoryCountRepository, stockRepository, unitOfWork);
|
||||
createInventoryCount = new CreateInventoryCount(inventoryCountRepository, stockRepository, unitOfWork, authPort);
|
||||
when(authPort.can(any(ActorId.class), any())).thenReturn(true);
|
||||
}
|
||||
|
||||
private void stubUnitOfWork() {
|
||||
|
|
@ -86,7 +91,7 @@ class CreateInventoryCountTest {
|
|||
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
||||
.thenReturn(Result.success(null));
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
var count = result.unsafeGetValue();
|
||||
|
|
@ -111,7 +116,7 @@ class CreateInventoryCountTest {
|
|||
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
||||
.thenReturn(Result.success(null));
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().countItems()).isEmpty();
|
||||
|
|
@ -125,7 +130,7 @@ class CreateInventoryCountTest {
|
|||
when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
|
||||
.thenReturn(Result.success(true));
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.ActiveCountExists.class);
|
||||
|
|
@ -136,7 +141,7 @@ class CreateInventoryCountTest {
|
|||
void shouldFailWhenStorageLocationIdInvalid() {
|
||||
var cmd = new CreateInventoryCountCommand("", LocalDate.now().toString(), "user-1");
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStorageLocationId.class);
|
||||
|
|
@ -147,7 +152,7 @@ class CreateInventoryCountTest {
|
|||
void shouldFailWhenCountDateInFuture() {
|
||||
var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().plusDays(1).toString(), "user-1");
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.CountDateInFuture.class);
|
||||
|
|
@ -161,7 +166,7 @@ class CreateInventoryCountTest {
|
|||
when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||
|
|
@ -177,7 +182,7 @@ class CreateInventoryCountTest {
|
|||
when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1")))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("stock db down")));
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||
|
|
@ -196,15 +201,15 @@ class CreateInventoryCountTest {
|
|||
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("save failed")));
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should handle stock without batches (zero quantity)")
|
||||
void shouldHandleStockWithoutBatches() {
|
||||
@DisplayName("should skip stocks without batches")
|
||||
void shouldSkipStocksWithoutBatches() {
|
||||
stubUnitOfWork();
|
||||
var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), "user-1");
|
||||
|
||||
|
|
@ -221,12 +226,10 @@ class CreateInventoryCountTest {
|
|||
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
||||
.thenReturn(Result.success(null));
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().countItems()).hasSize(1);
|
||||
assertThat(result.unsafeGetValue().countItems().getFirst().expectedQuantity().amount())
|
||||
.isEqualByComparingTo(BigDecimal.ZERO);
|
||||
assertThat(result.unsafeGetValue().countItems()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -262,7 +265,7 @@ class CreateInventoryCountTest {
|
|||
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
||||
.thenReturn(Result.success(null));
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().countItems()).hasSize(2);
|
||||
|
|
@ -273,7 +276,7 @@ class CreateInventoryCountTest {
|
|||
void shouldFailWhenInitiatedByNull() {
|
||||
var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), null);
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInitiatedBy.class);
|
||||
|
|
@ -284,7 +287,7 @@ class CreateInventoryCountTest {
|
|||
void shouldFailWhenCountDateUnparseable() {
|
||||
var cmd = new CreateInventoryCountCommand("location-1", "not-a-date", "user-1");
|
||||
|
||||
var result = createInventoryCount.execute(cmd);
|
||||
var result = createInventoryCount.execute(cmd, actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidCountDate.class);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package de.effigenix.application.inventory;
|
|||
import de.effigenix.domain.inventory.*;
|
||||
import de.effigenix.shared.common.RepositoryError;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import de.effigenix.shared.security.AuthorizationPort;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -16,6 +18,7 @@ import java.util.List;
|
|||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
|
|
@ -23,13 +26,16 @@ import static org.mockito.Mockito.when;
|
|||
class GetInventoryCountTest {
|
||||
|
||||
@Mock private InventoryCountRepository inventoryCountRepository;
|
||||
@Mock private AuthorizationPort authPort;
|
||||
|
||||
private GetInventoryCount getInventoryCount;
|
||||
private InventoryCount existingCount;
|
||||
private final ActorId actorId = ActorId.of("user-1");
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
getInventoryCount = new GetInventoryCount(inventoryCountRepository);
|
||||
getInventoryCount = new GetInventoryCount(inventoryCountRepository, authPort);
|
||||
when(authPort.can(any(ActorId.class), any())).thenReturn(true);
|
||||
|
||||
existingCount = InventoryCount.reconstitute(
|
||||
InventoryCountId.of("count-1"),
|
||||
|
|
@ -48,7 +54,7 @@ class GetInventoryCountTest {
|
|||
when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
|
||||
.thenReturn(Result.success(Optional.of(existingCount)));
|
||||
|
||||
var result = getInventoryCount.execute("count-1");
|
||||
var result = getInventoryCount.execute("count-1", actorId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue().id().value()).isEqualTo("count-1");
|
||||
|
|
@ -60,7 +66,7 @@ class GetInventoryCountTest {
|
|||
when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
|
||||
.thenReturn(Result.success(Optional.empty()));
|
||||
|
||||
var result = getInventoryCount.execute("count-1");
|
||||
var result = getInventoryCount.execute("count-1", actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InventoryCountNotFound.class);
|
||||
|
|
@ -72,36 +78,36 @@ class GetInventoryCountTest {
|
|||
when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = getInventoryCount.execute("count-1");
|
||||
var result = getInventoryCount.execute("count-1", actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with InventoryCountNotFound when id is null")
|
||||
@DisplayName("should fail with InvalidInventoryCountId when id is null")
|
||||
void shouldFailWhenIdIsNull() {
|
||||
var result = getInventoryCount.execute(null);
|
||||
var result = getInventoryCount.execute(null, actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InventoryCountNotFound.class);
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInventoryCountId.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with InventoryCountNotFound when id is blank")
|
||||
@DisplayName("should fail with InvalidInventoryCountId when id is blank")
|
||||
void shouldFailWhenIdIsBlank() {
|
||||
var result = getInventoryCount.execute(" ");
|
||||
var result = getInventoryCount.execute(" ", actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InventoryCountNotFound.class);
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInventoryCountId.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should fail with InventoryCountNotFound when id is empty string")
|
||||
@DisplayName("should fail with InvalidInventoryCountId when id is empty string")
|
||||
void shouldFailWhenIdIsEmpty() {
|
||||
var result = getInventoryCount.execute("");
|
||||
var result = getInventoryCount.execute("", actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InventoryCountNotFound.class);
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInventoryCountId.class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package de.effigenix.application.inventory;
|
|||
import de.effigenix.domain.inventory.*;
|
||||
import de.effigenix.shared.common.RepositoryError;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import de.effigenix.shared.security.AuthorizationPort;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -15,6 +17,7 @@ import java.time.LocalDate;
|
|||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
|
|
@ -22,14 +25,17 @@ import static org.mockito.Mockito.*;
|
|||
class ListInventoryCountsTest {
|
||||
|
||||
@Mock private InventoryCountRepository inventoryCountRepository;
|
||||
@Mock private AuthorizationPort authPort;
|
||||
|
||||
private ListInventoryCounts listInventoryCounts;
|
||||
private InventoryCount count1;
|
||||
private InventoryCount count2;
|
||||
private final ActorId actorId = ActorId.of("user-1");
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
listInventoryCounts = new ListInventoryCounts(inventoryCountRepository);
|
||||
listInventoryCounts = new ListInventoryCounts(inventoryCountRepository, authPort);
|
||||
when(authPort.can(any(ActorId.class), any())).thenReturn(true);
|
||||
|
||||
count1 = InventoryCount.reconstitute(
|
||||
InventoryCountId.of("count-1"),
|
||||
|
|
@ -57,7 +63,7 @@ class ListInventoryCountsTest {
|
|||
void shouldReturnAllCountsWhenNoFilter() {
|
||||
when(inventoryCountRepository.findAll()).thenReturn(Result.success(List.of(count1, count2)));
|
||||
|
||||
var result = listInventoryCounts.execute(null);
|
||||
var result = listInventoryCounts.execute(null, actorId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(2);
|
||||
|
|
@ -70,7 +76,7 @@ class ListInventoryCountsTest {
|
|||
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1")))
|
||||
.thenReturn(Result.success(List.of(count1)));
|
||||
|
||||
var result = listInventoryCounts.execute("location-1");
|
||||
var result = listInventoryCounts.execute("location-1", actorId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).hasSize(1);
|
||||
|
|
@ -84,7 +90,7 @@ class ListInventoryCountsTest {
|
|||
when(inventoryCountRepository.findAll())
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = listInventoryCounts.execute(null);
|
||||
var result = listInventoryCounts.execute(null, actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||
|
|
@ -96,7 +102,7 @@ class ListInventoryCountsTest {
|
|||
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1")))
|
||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
||||
|
||||
var result = listInventoryCounts.execute("location-1");
|
||||
var result = listInventoryCounts.execute("location-1", actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||
|
|
@ -108,7 +114,7 @@ class ListInventoryCountsTest {
|
|||
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("unknown")))
|
||||
.thenReturn(Result.success(List.of()));
|
||||
|
||||
var result = listInventoryCounts.execute("unknown");
|
||||
var result = listInventoryCounts.execute("unknown", actorId);
|
||||
|
||||
assertThat(result.isSuccess()).isTrue();
|
||||
assertThat(result.unsafeGetValue()).isEmpty();
|
||||
|
|
@ -117,7 +123,7 @@ class ListInventoryCountsTest {
|
|||
@Test
|
||||
@DisplayName("should fail with InvalidStorageLocationId for blank storageLocationId")
|
||||
void shouldFailWhenBlankStorageLocationId() {
|
||||
var result = listInventoryCounts.execute(" ");
|
||||
var result = listInventoryCounts.execute(" ", actorId);
|
||||
|
||||
assertThat(result.isFailure()).isTrue();
|
||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStorageLocationId.class);
|
||||
|
|
|
|||
|
|
@ -429,6 +429,20 @@ class InventoryCountTest {
|
|||
assertThat(item.deviation()).isEqualByComparingTo(new BigDecimal("2.5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should return null when UOMs differ")
|
||||
void shouldReturnNullWhenUomsDiffer() {
|
||||
var item = CountItem.reconstitute(
|
||||
CountItemId.of("item-1"),
|
||||
ArticleId.of("article-1"),
|
||||
Quantity.reconstitute(new BigDecimal("10.0"), UnitOfMeasure.KILOGRAM),
|
||||
Quantity.reconstitute(new BigDecimal("8.0"), UnitOfMeasure.LITER)
|
||||
);
|
||||
|
||||
assertThat(item.isCounted()).isTrue();
|
||||
assertThat(item.deviation()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should compute zero deviation (actual == expected)")
|
||||
void shouldComputeZeroDeviation() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue