mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 10:19: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
|
|
@ -4,22 +4,31 @@ import de.effigenix.application.inventory.command.CreateInventoryCountCommand;
|
||||||
import de.effigenix.domain.inventory.*;
|
import de.effigenix.domain.inventory.*;
|
||||||
import de.effigenix.shared.common.Result;
|
import de.effigenix.shared.common.Result;
|
||||||
import de.effigenix.shared.persistence.UnitOfWork;
|
import de.effigenix.shared.persistence.UnitOfWork;
|
||||||
|
import de.effigenix.shared.security.ActorId;
|
||||||
|
import de.effigenix.shared.security.AuthorizationPort;
|
||||||
|
|
||||||
public class CreateInventoryCount {
|
public class CreateInventoryCount {
|
||||||
|
|
||||||
private final InventoryCountRepository inventoryCountRepository;
|
private final InventoryCountRepository inventoryCountRepository;
|
||||||
private final StockRepository stockRepository;
|
private final StockRepository stockRepository;
|
||||||
private final UnitOfWork unitOfWork;
|
private final UnitOfWork unitOfWork;
|
||||||
|
private final AuthorizationPort authPort;
|
||||||
|
|
||||||
public CreateInventoryCount(InventoryCountRepository inventoryCountRepository,
|
public CreateInventoryCount(InventoryCountRepository inventoryCountRepository,
|
||||||
StockRepository stockRepository,
|
StockRepository stockRepository,
|
||||||
UnitOfWork unitOfWork) {
|
UnitOfWork unitOfWork,
|
||||||
|
AuthorizationPort authPort) {
|
||||||
this.inventoryCountRepository = inventoryCountRepository;
|
this.inventoryCountRepository = inventoryCountRepository;
|
||||||
this.stockRepository = stockRepository;
|
this.stockRepository = stockRepository;
|
||||||
this.unitOfWork = unitOfWork;
|
this.unitOfWork = unitOfWork;
|
||||||
|
this.authPort = authPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<InventoryCountError, InventoryCount> execute(CreateInventoryCountCommand cmd, ActorId actorId) {
|
||||||
|
if (!authPort.can(actorId, InventoryAction.INVENTORY_COUNT_WRITE)) {
|
||||||
|
return Result.failure(new InventoryCountError.Unauthorized("Not authorized to create inventory counts"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<InventoryCountError, InventoryCount> execute(CreateInventoryCountCommand cmd) {
|
|
||||||
// 1. Draft aus Command bauen
|
// 1. Draft aus Command bauen
|
||||||
var draft = new InventoryCountDraft(cmd.storageLocationId(), cmd.countDate(), cmd.initiatedBy());
|
var draft = new InventoryCountDraft(cmd.storageLocationId(), cmd.countDate(), cmd.initiatedBy());
|
||||||
|
|
||||||
|
|
@ -48,14 +57,16 @@ public class CreateInventoryCount {
|
||||||
{ return Result.failure(new InventoryCountError.RepositoryFailure(err.message())); }
|
{ return Result.failure(new InventoryCountError.RepositoryFailure(err.message())); }
|
||||||
case Result.Success(var stocks) -> {
|
case Result.Success(var stocks) -> {
|
||||||
for (Stock stock : stocks) {
|
for (Stock stock : stocks) {
|
||||||
// Gesamtmenge aus Batches berechnen
|
// Skip stocks without batches – nothing to count
|
||||||
|
if (stock.batches().isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var totalAmount = stock.batches().stream()
|
var totalAmount = stock.batches().stream()
|
||||||
.map(b -> b.quantity().amount())
|
.map(b -> b.quantity().amount())
|
||||||
.reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
|
.reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
|
||||||
|
|
||||||
String unit = stock.batches().isEmpty()
|
String unit = stock.batches().getFirst().quantity().uom().name();
|
||||||
? "KILOGRAM"
|
|
||||||
: stock.batches().getFirst().quantity().uom().name();
|
|
||||||
|
|
||||||
var itemDraft = new CountItemDraft(
|
var itemDraft = new CountItemDraft(
|
||||||
stock.articleId().value(),
|
stock.articleId().value(),
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,31 @@
|
||||||
package de.effigenix.application.inventory;
|
package de.effigenix.application.inventory;
|
||||||
|
|
||||||
|
import de.effigenix.domain.inventory.InventoryAction;
|
||||||
import de.effigenix.domain.inventory.InventoryCount;
|
import de.effigenix.domain.inventory.InventoryCount;
|
||||||
import de.effigenix.domain.inventory.InventoryCountError;
|
import de.effigenix.domain.inventory.InventoryCountError;
|
||||||
import de.effigenix.domain.inventory.InventoryCountId;
|
import de.effigenix.domain.inventory.InventoryCountId;
|
||||||
import de.effigenix.domain.inventory.InventoryCountRepository;
|
import de.effigenix.domain.inventory.InventoryCountRepository;
|
||||||
import de.effigenix.shared.common.Result;
|
import de.effigenix.shared.common.Result;
|
||||||
|
import de.effigenix.shared.security.ActorId;
|
||||||
|
import de.effigenix.shared.security.AuthorizationPort;
|
||||||
|
|
||||||
public class GetInventoryCount {
|
public class GetInventoryCount {
|
||||||
|
|
||||||
private final InventoryCountRepository inventoryCountRepository;
|
private final InventoryCountRepository inventoryCountRepository;
|
||||||
|
private final AuthorizationPort authPort;
|
||||||
|
|
||||||
public GetInventoryCount(InventoryCountRepository inventoryCountRepository) {
|
public GetInventoryCount(InventoryCountRepository inventoryCountRepository, AuthorizationPort authPort) {
|
||||||
this.inventoryCountRepository = inventoryCountRepository;
|
this.inventoryCountRepository = inventoryCountRepository;
|
||||||
|
this.authPort = authPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<InventoryCountError, InventoryCount> execute(String inventoryCountId, ActorId actorId) {
|
||||||
|
if (!authPort.can(actorId, InventoryAction.INVENTORY_COUNT_READ)) {
|
||||||
|
return Result.failure(new InventoryCountError.Unauthorized("Not authorized to view inventory counts"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<InventoryCountError, InventoryCount> execute(String inventoryCountId) {
|
|
||||||
if (inventoryCountId == null || inventoryCountId.isBlank()) {
|
if (inventoryCountId == null || inventoryCountId.isBlank()) {
|
||||||
return Result.failure(new InventoryCountError.InventoryCountNotFound(inventoryCountId));
|
return Result.failure(new InventoryCountError.InvalidInventoryCountId("must not be blank"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return switch (inventoryCountRepository.findById(InventoryCountId.of(inventoryCountId))) {
|
return switch (inventoryCountRepository.findById(InventoryCountId.of(inventoryCountId))) {
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,26 @@ package de.effigenix.application.inventory;
|
||||||
import de.effigenix.domain.inventory.*;
|
import de.effigenix.domain.inventory.*;
|
||||||
import de.effigenix.shared.common.RepositoryError;
|
import de.effigenix.shared.common.RepositoryError;
|
||||||
import de.effigenix.shared.common.Result;
|
import de.effigenix.shared.common.Result;
|
||||||
|
import de.effigenix.shared.security.ActorId;
|
||||||
|
import de.effigenix.shared.security.AuthorizationPort;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ListInventoryCounts {
|
public class ListInventoryCounts {
|
||||||
|
|
||||||
private final InventoryCountRepository inventoryCountRepository;
|
private final InventoryCountRepository inventoryCountRepository;
|
||||||
|
private final AuthorizationPort authPort;
|
||||||
|
|
||||||
public ListInventoryCounts(InventoryCountRepository inventoryCountRepository) {
|
public ListInventoryCounts(InventoryCountRepository inventoryCountRepository, AuthorizationPort authPort) {
|
||||||
this.inventoryCountRepository = inventoryCountRepository;
|
this.inventoryCountRepository = inventoryCountRepository;
|
||||||
|
this.authPort = authPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<InventoryCountError, List<InventoryCount>> execute(String storageLocationId, ActorId actorId) {
|
||||||
|
if (!authPort.can(actorId, InventoryAction.INVENTORY_COUNT_READ)) {
|
||||||
|
return Result.failure(new InventoryCountError.Unauthorized("Not authorized to view inventory counts"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<InventoryCountError, List<InventoryCount>> execute(String storageLocationId) {
|
|
||||||
if (storageLocationId != null) {
|
if (storageLocationId != null) {
|
||||||
StorageLocationId locId;
|
StorageLocationId locId;
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -74,12 +74,18 @@ public class CountItem {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed deviation: actualQuantity - expectedQuantity.
|
* Computed deviation: actualQuantity - expectedQuantity.
|
||||||
* Returns null if actualQuantity has not been set yet.
|
* Returns null if actualQuantity has not been set yet or if UOMs are incompatible.
|
||||||
|
*
|
||||||
|
* Invariant: actualQuantity and expectedQuantity should always share the same UOM.
|
||||||
|
* A UOM mismatch indicates a data integrity issue.
|
||||||
*/
|
*/
|
||||||
public BigDecimal deviation() {
|
public BigDecimal deviation() {
|
||||||
if (actualQuantity == null) {
|
if (actualQuantity == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (actualQuantity.uom() != expectedQuantity.uom()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return actualQuantity.amount().subtract(expectedQuantity.amount());
|
return actualQuantity.amount().subtract(expectedQuantity.amount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,11 @@ public sealed interface InventoryCountError {
|
||||||
@Override public String message() { return "Counter must not be the same person who initiated the count"; }
|
@Override public String message() { return "Counter must not be the same person who initiated the count"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record InvalidInventoryCountId(String reason) implements InventoryCountError {
|
||||||
|
@Override public String code() { return "INVALID_INVENTORY_COUNT_ID"; }
|
||||||
|
@Override public String message() { return "Invalid inventory count ID: " + reason; }
|
||||||
|
}
|
||||||
|
|
||||||
record InventoryCountNotFound(String id) implements InventoryCountError {
|
record InventoryCountNotFound(String id) implements InventoryCountError {
|
||||||
@Override public String code() { return "INVENTORY_COUNT_NOT_FOUND"; }
|
@Override public String code() { return "INVENTORY_COUNT_NOT_FOUND"; }
|
||||||
@Override public String message() { return "Inventory count not found: " + id; }
|
@Override public String message() { return "Inventory count not found: " + id; }
|
||||||
|
|
|
||||||
|
|
@ -159,17 +159,17 @@ public class InventoryUseCaseConfiguration {
|
||||||
// ==================== InventoryCount Use Cases ====================
|
// ==================== InventoryCount Use Cases ====================
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public CreateInventoryCount createInventoryCount(InventoryCountRepository inventoryCountRepository, StockRepository stockRepository, UnitOfWork unitOfWork) {
|
public CreateInventoryCount createInventoryCount(InventoryCountRepository inventoryCountRepository, StockRepository stockRepository, UnitOfWork unitOfWork, AuthorizationPort authorizationPort) {
|
||||||
return new CreateInventoryCount(inventoryCountRepository, stockRepository, unitOfWork);
|
return new CreateInventoryCount(inventoryCountRepository, stockRepository, unitOfWork, authorizationPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public GetInventoryCount getInventoryCount(InventoryCountRepository inventoryCountRepository) {
|
public GetInventoryCount getInventoryCount(InventoryCountRepository inventoryCountRepository, AuthorizationPort authorizationPort) {
|
||||||
return new GetInventoryCount(inventoryCountRepository);
|
return new GetInventoryCount(inventoryCountRepository, authorizationPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ListInventoryCounts listInventoryCounts(InventoryCountRepository inventoryCountRepository) {
|
public ListInventoryCounts listInventoryCounts(InventoryCountRepository inventoryCountRepository, AuthorizationPort authorizationPort) {
|
||||||
return new ListInventoryCounts(inventoryCountRepository);
|
return new ListInventoryCounts(inventoryCountRepository, authorizationPort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
||||||
}
|
}
|
||||||
return Result.success(Optional.of(loadChildren(countOpt.get(), id.value())));
|
return Result.success(Optional.of(loadChildren(countOpt.get(), id.value())));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.trace("Database error in findById", e);
|
logger.warn("Database error in findById", e);
|
||||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +58,7 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
||||||
.list();
|
.list();
|
||||||
return Result.success(loadChildrenForAll(counts));
|
return Result.success(loadChildrenForAll(counts));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.trace("Database error in findAll", e);
|
logger.warn("Database error in findAll", e);
|
||||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +72,7 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
||||||
.list();
|
.list();
|
||||||
return Result.success(loadChildrenForAll(counts));
|
return Result.success(loadChildrenForAll(counts));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.trace("Database error in findByStorageLocationId", e);
|
logger.warn("Database error in findByStorageLocationId", e);
|
||||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +90,7 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
||||||
.single();
|
.single();
|
||||||
return Result.success(count > 0);
|
return Result.success(count > 0);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.trace("Database error in existsActiveByStorageLocationId", e);
|
logger.warn("Database error in existsActiveByStorageLocationId", e);
|
||||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +122,7 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
||||||
|
|
||||||
return Result.success(null);
|
return Result.success(null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.trace("Database error in save", e);
|
logger.warn("Database error in save", e);
|
||||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -142,12 +142,41 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
||||||
private void saveChildren(InventoryCount count) {
|
private void saveChildren(InventoryCount count) {
|
||||||
String countId = count.id().value();
|
String countId = count.id().value();
|
||||||
|
|
||||||
// Delete + re-insert count items
|
// Remove orphaned items no longer in the aggregate
|
||||||
|
List<String> currentIds = count.countItems().stream()
|
||||||
|
.map(item -> item.id().value())
|
||||||
|
.toList();
|
||||||
|
if (currentIds.isEmpty()) {
|
||||||
jdbc.sql("DELETE FROM count_items WHERE inventory_count_id = :countId")
|
jdbc.sql("DELETE FROM count_items WHERE inventory_count_id = :countId")
|
||||||
.param("countId", countId)
|
.param("countId", countId)
|
||||||
.update();
|
.update();
|
||||||
|
} else {
|
||||||
|
jdbc.sql("DELETE FROM count_items WHERE inventory_count_id = :countId AND id NOT IN (:ids)")
|
||||||
|
.param("countId", countId)
|
||||||
|
.param("ids", currentIds)
|
||||||
|
.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert each item (UPDATE → INSERT)
|
||||||
for (CountItem item : count.countItems()) {
|
for (CountItem item : count.countItems()) {
|
||||||
|
int rows = jdbc.sql("""
|
||||||
|
UPDATE count_items
|
||||||
|
SET article_id = :articleId,
|
||||||
|
expected_quantity_amount = :expectedQuantityAmount,
|
||||||
|
expected_quantity_unit = :expectedQuantityUnit,
|
||||||
|
actual_quantity_amount = :actualQuantityAmount,
|
||||||
|
actual_quantity_unit = :actualQuantityUnit
|
||||||
|
WHERE id = :id
|
||||||
|
""")
|
||||||
|
.param("id", item.id().value())
|
||||||
|
.param("articleId", item.articleId().value())
|
||||||
|
.param("expectedQuantityAmount", item.expectedQuantity().amount())
|
||||||
|
.param("expectedQuantityUnit", item.expectedQuantity().uom().name())
|
||||||
|
.param("actualQuantityAmount", item.actualQuantity() != null ? item.actualQuantity().amount() : null)
|
||||||
|
.param("actualQuantityUnit", item.actualQuantity() != null ? item.actualQuantity().uom().name() : null)
|
||||||
|
.update();
|
||||||
|
|
||||||
|
if (rows == 0) {
|
||||||
jdbc.sql("""
|
jdbc.sql("""
|
||||||
INSERT INTO count_items
|
INSERT INTO count_items
|
||||||
(id, inventory_count_id, article_id,
|
(id, inventory_count_id, article_id,
|
||||||
|
|
@ -167,6 +196,7 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
|
||||||
.update();
|
.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private InventoryCount loadChildren(InventoryCount count, String countId) {
|
private InventoryCount loadChildren(InventoryCount count, String countId) {
|
||||||
var items = jdbc.sql("SELECT * FROM count_items WHERE inventory_count_id = :countId")
|
var items = jdbc.sql("SELECT * FROM count_items WHERE inventory_count_id = :countId")
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import de.effigenix.application.inventory.command.CreateInventoryCountCommand;
|
||||||
import de.effigenix.domain.inventory.InventoryCountError;
|
import de.effigenix.domain.inventory.InventoryCountError;
|
||||||
import de.effigenix.infrastructure.inventory.web.dto.CreateInventoryCountRequest;
|
import de.effigenix.infrastructure.inventory.web.dto.CreateInventoryCountRequest;
|
||||||
import de.effigenix.infrastructure.inventory.web.dto.InventoryCountResponse;
|
import de.effigenix.infrastructure.inventory.web.dto.InventoryCountResponse;
|
||||||
|
import de.effigenix.shared.security.ActorId;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
@ -48,7 +49,7 @@ public class InventoryCountController {
|
||||||
authentication.getName()
|
authentication.getName()
|
||||||
);
|
);
|
||||||
|
|
||||||
var result = createInventoryCount.execute(cmd);
|
var result = createInventoryCount.execute(cmd, ActorId.of(authentication.getName()));
|
||||||
|
|
||||||
if (result.isFailure()) {
|
if (result.isFailure()) {
|
||||||
throw new InventoryCountDomainErrorException(result.unsafeGetError());
|
throw new InventoryCountDomainErrorException(result.unsafeGetError());
|
||||||
|
|
@ -60,8 +61,11 @@ public class InventoryCountController {
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@PreAuthorize("hasAuthority('INVENTORY_COUNT_READ')")
|
@PreAuthorize("hasAuthority('INVENTORY_COUNT_READ')")
|
||||||
public ResponseEntity<InventoryCountResponse> getInventoryCount(@PathVariable String id) {
|
public ResponseEntity<InventoryCountResponse> getInventoryCount(
|
||||||
var result = getInventoryCount.execute(id);
|
@PathVariable String id,
|
||||||
|
Authentication authentication
|
||||||
|
) {
|
||||||
|
var result = getInventoryCount.execute(id, ActorId.of(authentication.getName()));
|
||||||
|
|
||||||
if (result.isFailure()) {
|
if (result.isFailure()) {
|
||||||
throw new InventoryCountDomainErrorException(result.unsafeGetError());
|
throw new InventoryCountDomainErrorException(result.unsafeGetError());
|
||||||
|
|
@ -73,9 +77,10 @@ public class InventoryCountController {
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasAuthority('INVENTORY_COUNT_READ')")
|
@PreAuthorize("hasAuthority('INVENTORY_COUNT_READ')")
|
||||||
public ResponseEntity<List<InventoryCountResponse>> listInventoryCounts(
|
public ResponseEntity<List<InventoryCountResponse>> listInventoryCounts(
|
||||||
@RequestParam(required = false) String storageLocationId
|
@RequestParam(required = false) String storageLocationId,
|
||||||
|
Authentication authentication
|
||||||
) {
|
) {
|
||||||
var result = listInventoryCounts.execute(storageLocationId);
|
var result = listInventoryCounts.execute(storageLocationId, ActorId.of(authentication.getName()));
|
||||||
|
|
||||||
if (result.isFailure()) {
|
if (result.isFailure()) {
|
||||||
throw new InventoryCountDomainErrorException(result.unsafeGetError());
|
throw new InventoryCountDomainErrorException(result.unsafeGetError());
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ public final class InventoryErrorHttpStatusMapper {
|
||||||
case InventoryCountError.InvalidInitiatedBy e -> 400;
|
case InventoryCountError.InvalidInitiatedBy e -> 400;
|
||||||
case InventoryCountError.InvalidArticleId e -> 400;
|
case InventoryCountError.InvalidArticleId e -> 400;
|
||||||
case InventoryCountError.InvalidQuantity e -> 400;
|
case InventoryCountError.InvalidQuantity e -> 400;
|
||||||
|
case InventoryCountError.InvalidInventoryCountId e -> 400;
|
||||||
case InventoryCountError.Unauthorized e -> 403;
|
case InventoryCountError.Unauthorized e -> 403;
|
||||||
case InventoryCountError.RepositoryFailure e -> 500;
|
case InventoryCountError.RepositoryFailure e -> 500;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package de.effigenix.infrastructure.security;
|
package de.effigenix.infrastructure.security;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import de.effigenix.infrastructure.usermanagement.web.dto.ErrorResponse;
|
import de.effigenix.infrastructure.shared.web.exception.ErrorResponse;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package de.effigenix.infrastructure.security;
|
package de.effigenix.infrastructure.security;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import de.effigenix.infrastructure.usermanagement.web.dto.ErrorResponse;
|
import de.effigenix.infrastructure.shared.web.exception.ErrorResponse;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package de.effigenix.infrastructure.usermanagement.web.dto;
|
package de.effigenix.infrastructure.shared.web.exception;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package de.effigenix.infrastructure.usermanagement.web.exception;
|
package de.effigenix.infrastructure.shared.web.exception;
|
||||||
|
|
||||||
import de.effigenix.domain.inventory.InventoryCountError;
|
import de.effigenix.domain.inventory.InventoryCountError;
|
||||||
import de.effigenix.domain.inventory.StockMovementError;
|
import de.effigenix.domain.inventory.StockMovementError;
|
||||||
|
|
@ -30,7 +30,7 @@ import de.effigenix.infrastructure.masterdata.web.exception.MasterDataErrorHttpS
|
||||||
import de.effigenix.infrastructure.usermanagement.web.controller.AuthController;
|
import de.effigenix.infrastructure.usermanagement.web.controller.AuthController;
|
||||||
import de.effigenix.infrastructure.usermanagement.web.controller.RoleController;
|
import de.effigenix.infrastructure.usermanagement.web.controller.RoleController;
|
||||||
import de.effigenix.infrastructure.usermanagement.web.controller.UserController;
|
import de.effigenix.infrastructure.usermanagement.web.controller.UserController;
|
||||||
import de.effigenix.infrastructure.usermanagement.web.dto.ErrorResponse;
|
import de.effigenix.infrastructure.usermanagement.web.exception.UserErrorHttpStatusMapper;
|
||||||
|
|
||||||
import io.sentry.Sentry;
|
import io.sentry.Sentry;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
@ -372,10 +372,6 @@ public class GlobalExceptionHandler {
|
||||||
* Handles validation errors from @Valid annotations.
|
* Handles validation errors from @Valid annotations.
|
||||||
*
|
*
|
||||||
* Returns 400 Bad Request with list of validation errors.
|
* Returns 400 Bad Request with list of validation errors.
|
||||||
* Example:
|
|
||||||
* - Username is required
|
|
||||||
* - Email must be valid
|
|
||||||
* - Password must be at least 8 characters
|
|
||||||
*
|
*
|
||||||
* @param ex Validation exception
|
* @param ex Validation exception
|
||||||
* @param request HTTP request
|
* @param request HTTP request
|
||||||
|
|
@ -412,17 +408,6 @@ public class GlobalExceptionHandler {
|
||||||
.body(errorResponse);
|
.body(errorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles authentication errors (e.g., invalid JWT token).
|
|
||||||
*
|
|
||||||
* Returns 401 Unauthorized.
|
|
||||||
* This is typically caught by SecurityConfig's authenticationEntryPoint,
|
|
||||||
* but included here for completeness.
|
|
||||||
*
|
|
||||||
* @param ex Authentication exception
|
|
||||||
* @param request HTTP request
|
|
||||||
* @return Error response with 401 status
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(AuthenticationException.class)
|
@ExceptionHandler(AuthenticationException.class)
|
||||||
public ResponseEntity<ErrorResponse> handleAuthenticationError(
|
public ResponseEntity<ErrorResponse> handleAuthenticationError(
|
||||||
AuthenticationException ex,
|
AuthenticationException ex,
|
||||||
|
|
@ -442,16 +427,6 @@ public class GlobalExceptionHandler {
|
||||||
.body(errorResponse);
|
.body(errorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles authorization errors (missing permissions).
|
|
||||||
*
|
|
||||||
* Returns 403 Forbidden.
|
|
||||||
* Triggered when user lacks required permission for an action.
|
|
||||||
*
|
|
||||||
* @param ex Access denied exception
|
|
||||||
* @param request HTTP request
|
|
||||||
* @return Error response with 403 status
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(AccessDeniedException.class)
|
@ExceptionHandler(AccessDeniedException.class)
|
||||||
public ResponseEntity<ErrorResponse> handleAccessDeniedError(
|
public ResponseEntity<ErrorResponse> handleAccessDeniedError(
|
||||||
AccessDeniedException ex,
|
AccessDeniedException ex,
|
||||||
|
|
@ -471,15 +446,6 @@ public class GlobalExceptionHandler {
|
||||||
.body(errorResponse);
|
.body(errorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles illegal arguments (e.g., invalid UUID format).
|
|
||||||
*
|
|
||||||
* Returns 400 Bad Request.
|
|
||||||
*
|
|
||||||
* @param ex Illegal argument exception
|
|
||||||
* @param request HTTP request
|
|
||||||
* @return Error response with 400 status
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(IllegalArgumentException.class)
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
public ResponseEntity<ErrorResponse> handleIllegalArgumentError(
|
public ResponseEntity<ErrorResponse> handleIllegalArgumentError(
|
||||||
IllegalArgumentException ex,
|
IllegalArgumentException ex,
|
||||||
|
|
@ -499,18 +465,6 @@ public class GlobalExceptionHandler {
|
||||||
.body(errorResponse);
|
.body(errorResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles unexpected runtime errors.
|
|
||||||
*
|
|
||||||
* Returns 500 Internal Server Error.
|
|
||||||
* Logs full stack trace for debugging.
|
|
||||||
*
|
|
||||||
* IMPORTANT: Do not expose internal error details to clients in production!
|
|
||||||
*
|
|
||||||
* @param ex Runtime exception
|
|
||||||
* @param request HTTP request
|
|
||||||
* @return Error response with 500 status
|
|
||||||
*/
|
|
||||||
@ExceptionHandler(RuntimeException.class)
|
@ExceptionHandler(RuntimeException.class)
|
||||||
public ResponseEntity<ErrorResponse> handleRuntimeError(
|
public ResponseEntity<ErrorResponse> handleRuntimeError(
|
||||||
RuntimeException ex,
|
RuntimeException ex,
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<databaseChangeLog
|
||||||
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
||||||
|
|
||||||
|
<changeSet id="036-add-inventory-counts-composite-index" author="effigenix">
|
||||||
|
<createIndex indexName="idx_inventory_counts_location_status"
|
||||||
|
tableName="inventory_counts">
|
||||||
|
<column name="storage_location_id"/>
|
||||||
|
<column name="status"/>
|
||||||
|
</createIndex>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
|
|
@ -41,5 +41,6 @@
|
||||||
|
|
||||||
<include file="db/changelog/changes/034-create-inventory-counts-schema.xml"/>
|
<include file="db/changelog/changes/034-create-inventory-counts-schema.xml"/>
|
||||||
<include file="db/changelog/changes/035-seed-inventory-count-permissions.xml"/>
|
<include file="db/changelog/changes/035-seed-inventory-count-permissions.xml"/>
|
||||||
|
<include file="db/changelog/changes/036-add-inventory-counts-composite-index.xml"/>
|
||||||
|
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import de.effigenix.shared.common.RepositoryError;
|
||||||
import de.effigenix.shared.common.Result;
|
import de.effigenix.shared.common.Result;
|
||||||
import de.effigenix.shared.common.UnitOfMeasure;
|
import de.effigenix.shared.common.UnitOfMeasure;
|
||||||
import de.effigenix.shared.persistence.UnitOfWork;
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -31,12 +33,15 @@ class CreateInventoryCountTest {
|
||||||
@Mock private InventoryCountRepository inventoryCountRepository;
|
@Mock private InventoryCountRepository inventoryCountRepository;
|
||||||
@Mock private StockRepository stockRepository;
|
@Mock private StockRepository stockRepository;
|
||||||
@Mock private UnitOfWork unitOfWork;
|
@Mock private UnitOfWork unitOfWork;
|
||||||
|
@Mock private AuthorizationPort authPort;
|
||||||
|
|
||||||
private CreateInventoryCount createInventoryCount;
|
private CreateInventoryCount createInventoryCount;
|
||||||
|
private final ActorId actorId = ActorId.of("user-1");
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
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() {
|
private void stubUnitOfWork() {
|
||||||
|
|
@ -86,7 +91,7 @@ class CreateInventoryCountTest {
|
||||||
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
||||||
.thenReturn(Result.success(null));
|
.thenReturn(Result.success(null));
|
||||||
|
|
||||||
var result = createInventoryCount.execute(cmd);
|
var result = createInventoryCount.execute(cmd, actorId);
|
||||||
|
|
||||||
assertThat(result.isSuccess()).isTrue();
|
assertThat(result.isSuccess()).isTrue();
|
||||||
var count = result.unsafeGetValue();
|
var count = result.unsafeGetValue();
|
||||||
|
|
@ -111,7 +116,7 @@ class CreateInventoryCountTest {
|
||||||
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
||||||
.thenReturn(Result.success(null));
|
.thenReturn(Result.success(null));
|
||||||
|
|
||||||
var result = createInventoryCount.execute(cmd);
|
var result = createInventoryCount.execute(cmd, actorId);
|
||||||
|
|
||||||
assertThat(result.isSuccess()).isTrue();
|
assertThat(result.isSuccess()).isTrue();
|
||||||
assertThat(result.unsafeGetValue().countItems()).isEmpty();
|
assertThat(result.unsafeGetValue().countItems()).isEmpty();
|
||||||
|
|
@ -125,7 +130,7 @@ class CreateInventoryCountTest {
|
||||||
when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
|
when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
|
||||||
.thenReturn(Result.success(true));
|
.thenReturn(Result.success(true));
|
||||||
|
|
||||||
var result = createInventoryCount.execute(cmd);
|
var result = createInventoryCount.execute(cmd, actorId);
|
||||||
|
|
||||||
assertThat(result.isFailure()).isTrue();
|
assertThat(result.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.ActiveCountExists.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.ActiveCountExists.class);
|
||||||
|
|
@ -136,7 +141,7 @@ class CreateInventoryCountTest {
|
||||||
void shouldFailWhenStorageLocationIdInvalid() {
|
void shouldFailWhenStorageLocationIdInvalid() {
|
||||||
var cmd = new CreateInventoryCountCommand("", LocalDate.now().toString(), "user-1");
|
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.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStorageLocationId.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStorageLocationId.class);
|
||||||
|
|
@ -147,7 +152,7 @@ class CreateInventoryCountTest {
|
||||||
void shouldFailWhenCountDateInFuture() {
|
void shouldFailWhenCountDateInFuture() {
|
||||||
var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().plusDays(1).toString(), "user-1");
|
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.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.CountDateInFuture.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.CountDateInFuture.class);
|
||||||
|
|
@ -161,7 +166,7 @@ class CreateInventoryCountTest {
|
||||||
when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
|
when(inventoryCountRepository.existsActiveByStorageLocationId(StorageLocationId.of("location-1")))
|
||||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
.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.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||||
|
|
@ -177,7 +182,7 @@ class CreateInventoryCountTest {
|
||||||
when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1")))
|
when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1")))
|
||||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("stock db down")));
|
.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.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||||
|
|
@ -196,15 +201,15 @@ class CreateInventoryCountTest {
|
||||||
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
||||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("save failed")));
|
.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.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("should handle stock without batches (zero quantity)")
|
@DisplayName("should skip stocks without batches")
|
||||||
void shouldHandleStockWithoutBatches() {
|
void shouldSkipStocksWithoutBatches() {
|
||||||
stubUnitOfWork();
|
stubUnitOfWork();
|
||||||
var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), "user-1");
|
var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), "user-1");
|
||||||
|
|
||||||
|
|
@ -221,12 +226,10 @@ class CreateInventoryCountTest {
|
||||||
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
||||||
.thenReturn(Result.success(null));
|
.thenReturn(Result.success(null));
|
||||||
|
|
||||||
var result = createInventoryCount.execute(cmd);
|
var result = createInventoryCount.execute(cmd, actorId);
|
||||||
|
|
||||||
assertThat(result.isSuccess()).isTrue();
|
assertThat(result.isSuccess()).isTrue();
|
||||||
assertThat(result.unsafeGetValue().countItems()).hasSize(1);
|
assertThat(result.unsafeGetValue().countItems()).isEmpty();
|
||||||
assertThat(result.unsafeGetValue().countItems().getFirst().expectedQuantity().amount())
|
|
||||||
.isEqualByComparingTo(BigDecimal.ZERO);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -262,7 +265,7 @@ class CreateInventoryCountTest {
|
||||||
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
when(inventoryCountRepository.save(any(InventoryCount.class)))
|
||||||
.thenReturn(Result.success(null));
|
.thenReturn(Result.success(null));
|
||||||
|
|
||||||
var result = createInventoryCount.execute(cmd);
|
var result = createInventoryCount.execute(cmd, actorId);
|
||||||
|
|
||||||
assertThat(result.isSuccess()).isTrue();
|
assertThat(result.isSuccess()).isTrue();
|
||||||
assertThat(result.unsafeGetValue().countItems()).hasSize(2);
|
assertThat(result.unsafeGetValue().countItems()).hasSize(2);
|
||||||
|
|
@ -273,7 +276,7 @@ class CreateInventoryCountTest {
|
||||||
void shouldFailWhenInitiatedByNull() {
|
void shouldFailWhenInitiatedByNull() {
|
||||||
var cmd = new CreateInventoryCountCommand("location-1", LocalDate.now().toString(), null);
|
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.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInitiatedBy.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInitiatedBy.class);
|
||||||
|
|
@ -284,7 +287,7 @@ class CreateInventoryCountTest {
|
||||||
void shouldFailWhenCountDateUnparseable() {
|
void shouldFailWhenCountDateUnparseable() {
|
||||||
var cmd = new CreateInventoryCountCommand("location-1", "not-a-date", "user-1");
|
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.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidCountDate.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidCountDate.class);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package de.effigenix.application.inventory;
|
||||||
import de.effigenix.domain.inventory.*;
|
import de.effigenix.domain.inventory.*;
|
||||||
import de.effigenix.shared.common.RepositoryError;
|
import de.effigenix.shared.common.RepositoryError;
|
||||||
import de.effigenix.shared.common.Result;
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -16,6 +18,7 @@ import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
|
@ -23,13 +26,16 @@ import static org.mockito.Mockito.when;
|
||||||
class GetInventoryCountTest {
|
class GetInventoryCountTest {
|
||||||
|
|
||||||
@Mock private InventoryCountRepository inventoryCountRepository;
|
@Mock private InventoryCountRepository inventoryCountRepository;
|
||||||
|
@Mock private AuthorizationPort authPort;
|
||||||
|
|
||||||
private GetInventoryCount getInventoryCount;
|
private GetInventoryCount getInventoryCount;
|
||||||
private InventoryCount existingCount;
|
private InventoryCount existingCount;
|
||||||
|
private final ActorId actorId = ActorId.of("user-1");
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
getInventoryCount = new GetInventoryCount(inventoryCountRepository);
|
getInventoryCount = new GetInventoryCount(inventoryCountRepository, authPort);
|
||||||
|
when(authPort.can(any(ActorId.class), any())).thenReturn(true);
|
||||||
|
|
||||||
existingCount = InventoryCount.reconstitute(
|
existingCount = InventoryCount.reconstitute(
|
||||||
InventoryCountId.of("count-1"),
|
InventoryCountId.of("count-1"),
|
||||||
|
|
@ -48,7 +54,7 @@ class GetInventoryCountTest {
|
||||||
when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
|
when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
|
||||||
.thenReturn(Result.success(Optional.of(existingCount)));
|
.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.isSuccess()).isTrue();
|
||||||
assertThat(result.unsafeGetValue().id().value()).isEqualTo("count-1");
|
assertThat(result.unsafeGetValue().id().value()).isEqualTo("count-1");
|
||||||
|
|
@ -60,7 +66,7 @@ class GetInventoryCountTest {
|
||||||
when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
|
when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
|
||||||
.thenReturn(Result.success(Optional.empty()));
|
.thenReturn(Result.success(Optional.empty()));
|
||||||
|
|
||||||
var result = getInventoryCount.execute("count-1");
|
var result = getInventoryCount.execute("count-1", actorId);
|
||||||
|
|
||||||
assertThat(result.isFailure()).isTrue();
|
assertThat(result.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InventoryCountNotFound.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InventoryCountNotFound.class);
|
||||||
|
|
@ -72,36 +78,36 @@ class GetInventoryCountTest {
|
||||||
when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
|
when(inventoryCountRepository.findById(InventoryCountId.of("count-1")))
|
||||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
.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.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("should fail with InventoryCountNotFound when id is null")
|
@DisplayName("should fail with InvalidInventoryCountId when id is null")
|
||||||
void shouldFailWhenIdIsNull() {
|
void shouldFailWhenIdIsNull() {
|
||||||
var result = getInventoryCount.execute(null);
|
var result = getInventoryCount.execute(null, actorId);
|
||||||
|
|
||||||
assertThat(result.isFailure()).isTrue();
|
assertThat(result.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InventoryCountNotFound.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInventoryCountId.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("should fail with InventoryCountNotFound when id is blank")
|
@DisplayName("should fail with InvalidInventoryCountId when id is blank")
|
||||||
void shouldFailWhenIdIsBlank() {
|
void shouldFailWhenIdIsBlank() {
|
||||||
var result = getInventoryCount.execute(" ");
|
var result = getInventoryCount.execute(" ", actorId);
|
||||||
|
|
||||||
assertThat(result.isFailure()).isTrue();
|
assertThat(result.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InventoryCountNotFound.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidInventoryCountId.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("should fail with InventoryCountNotFound when id is empty string")
|
@DisplayName("should fail with InvalidInventoryCountId when id is empty string")
|
||||||
void shouldFailWhenIdIsEmpty() {
|
void shouldFailWhenIdIsEmpty() {
|
||||||
var result = getInventoryCount.execute("");
|
var result = getInventoryCount.execute("", actorId);
|
||||||
|
|
||||||
assertThat(result.isFailure()).isTrue();
|
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.domain.inventory.*;
|
||||||
import de.effigenix.shared.common.RepositoryError;
|
import de.effigenix.shared.common.RepositoryError;
|
||||||
import de.effigenix.shared.common.Result;
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
@ -15,6 +17,7 @@ import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
|
@ -22,14 +25,17 @@ import static org.mockito.Mockito.*;
|
||||||
class ListInventoryCountsTest {
|
class ListInventoryCountsTest {
|
||||||
|
|
||||||
@Mock private InventoryCountRepository inventoryCountRepository;
|
@Mock private InventoryCountRepository inventoryCountRepository;
|
||||||
|
@Mock private AuthorizationPort authPort;
|
||||||
|
|
||||||
private ListInventoryCounts listInventoryCounts;
|
private ListInventoryCounts listInventoryCounts;
|
||||||
private InventoryCount count1;
|
private InventoryCount count1;
|
||||||
private InventoryCount count2;
|
private InventoryCount count2;
|
||||||
|
private final ActorId actorId = ActorId.of("user-1");
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
listInventoryCounts = new ListInventoryCounts(inventoryCountRepository);
|
listInventoryCounts = new ListInventoryCounts(inventoryCountRepository, authPort);
|
||||||
|
when(authPort.can(any(ActorId.class), any())).thenReturn(true);
|
||||||
|
|
||||||
count1 = InventoryCount.reconstitute(
|
count1 = InventoryCount.reconstitute(
|
||||||
InventoryCountId.of("count-1"),
|
InventoryCountId.of("count-1"),
|
||||||
|
|
@ -57,7 +63,7 @@ class ListInventoryCountsTest {
|
||||||
void shouldReturnAllCountsWhenNoFilter() {
|
void shouldReturnAllCountsWhenNoFilter() {
|
||||||
when(inventoryCountRepository.findAll()).thenReturn(Result.success(List.of(count1, count2)));
|
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.isSuccess()).isTrue();
|
||||||
assertThat(result.unsafeGetValue()).hasSize(2);
|
assertThat(result.unsafeGetValue()).hasSize(2);
|
||||||
|
|
@ -70,7 +76,7 @@ class ListInventoryCountsTest {
|
||||||
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1")))
|
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1")))
|
||||||
.thenReturn(Result.success(List.of(count1)));
|
.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.isSuccess()).isTrue();
|
||||||
assertThat(result.unsafeGetValue()).hasSize(1);
|
assertThat(result.unsafeGetValue()).hasSize(1);
|
||||||
|
|
@ -84,7 +90,7 @@ class ListInventoryCountsTest {
|
||||||
when(inventoryCountRepository.findAll())
|
when(inventoryCountRepository.findAll())
|
||||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
.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.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||||
|
|
@ -96,7 +102,7 @@ class ListInventoryCountsTest {
|
||||||
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1")))
|
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1")))
|
||||||
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
|
.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.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
|
||||||
|
|
@ -108,7 +114,7 @@ class ListInventoryCountsTest {
|
||||||
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("unknown")))
|
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("unknown")))
|
||||||
.thenReturn(Result.success(List.of()));
|
.thenReturn(Result.success(List.of()));
|
||||||
|
|
||||||
var result = listInventoryCounts.execute("unknown");
|
var result = listInventoryCounts.execute("unknown", actorId);
|
||||||
|
|
||||||
assertThat(result.isSuccess()).isTrue();
|
assertThat(result.isSuccess()).isTrue();
|
||||||
assertThat(result.unsafeGetValue()).isEmpty();
|
assertThat(result.unsafeGetValue()).isEmpty();
|
||||||
|
|
@ -117,7 +123,7 @@ class ListInventoryCountsTest {
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("should fail with InvalidStorageLocationId for blank storageLocationId")
|
@DisplayName("should fail with InvalidStorageLocationId for blank storageLocationId")
|
||||||
void shouldFailWhenBlankStorageLocationId() {
|
void shouldFailWhenBlankStorageLocationId() {
|
||||||
var result = listInventoryCounts.execute(" ");
|
var result = listInventoryCounts.execute(" ", actorId);
|
||||||
|
|
||||||
assertThat(result.isFailure()).isTrue();
|
assertThat(result.isFailure()).isTrue();
|
||||||
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStorageLocationId.class);
|
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStorageLocationId.class);
|
||||||
|
|
|
||||||
|
|
@ -429,6 +429,20 @@ class InventoryCountTest {
|
||||||
assertThat(item.deviation()).isEqualByComparingTo(new BigDecimal("2.5"));
|
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
|
@Test
|
||||||
@DisplayName("should compute zero deviation (actual == expected)")
|
@DisplayName("should compute zero deviation (actual == expected)")
|
||||||
void shouldComputeZeroDeviation() {
|
void shouldComputeZeroDeviation() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue