1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 10:19:35 +01:00

feat: Paginierung für alle GET-List-Endpoints (#61)

Einheitliches Paginierungs-Pattern mit page, size und Multi-Field sort
für alle 14 List-Endpoints. Response-Format ändert sich von [...] zu
{ content: [...], page: { number, size, totalElements, totalPages } }.

Backend:
- Shared Kernel: Page<T>, PageRequest, SortField, SortDirection
- PaginationHelper (SQL ORDER BY mit Whitelist), PageResponse DTO
- Paginated Methoden in allen 14 Domain-Repos + JDBC-Implementierungen
- Safety-Limit (500) für findAllBelowMinimumLevel/ExpiryRelevantBatches
- Alle List-Use-Cases akzeptieren PageRequest, liefern Page<T>
- Alle Controller mit page/size/sort Query-Params + PageResponse

Frontend:
- PagedResponse<T> Type auf nested page-Format aktualisiert
- Alle 14 API-Client-Resourcen liefern PagedResponse mit PaginationParams
- Alle Hooks mit Pagination-State (currentPage, totalPages, pageSize)
- Alle List-Screens mit Seiten-Navigation (Pfeiltasten) und Footer

Loadtest:
- Podman-Support im justfile (DOCKER_HOST auto-detect)
- Verschärfte Performance-Schwellwerte basierend auf Ist-Werten
This commit is contained in:
Sebastian Frick 2026-03-20 16:33:20 +01:00
parent fc4faafd57
commit 72979c9537
151 changed files with 2880 additions and 1120 deletions

View file

@ -1,13 +1,13 @@
package de.effigenix.application.inventory; package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
import java.util.List;
public class ListInventoryCounts { public class ListInventoryCounts {
private final InventoryCountRepository inventoryCountRepository; private final InventoryCountRepository inventoryCountRepository;
@ -18,7 +18,7 @@ public class ListInventoryCounts {
this.authPort = authPort; this.authPort = authPort;
} }
public Result<InventoryCountError, List<InventoryCount>> execute(String storageLocationId, String status, ActorId actorId) { public Result<InventoryCountError, Page<InventoryCount>> execute(String status, ActorId actorId, PageRequest pageRequest) {
if (!authPort.can(actorId, InventoryAction.INVENTORY_COUNT_READ)) { if (!authPort.can(actorId, InventoryAction.INVENTORY_COUNT_READ)) {
return Result.failure(new InventoryCountError.Unauthorized("Not authorized to view inventory counts")); return Result.failure(new InventoryCountError.Unauthorized("Not authorized to view inventory counts"));
} }
@ -32,34 +32,13 @@ public class ListInventoryCounts {
} }
} }
if (storageLocationId != null) { return mapResult(inventoryCountRepository.findAll(parsedStatus, pageRequest));
StorageLocationId locId;
try {
locId = StorageLocationId.of(storageLocationId);
} catch (IllegalArgumentException e) {
return Result.failure(new InventoryCountError.InvalidStorageLocationId(e.getMessage()));
}
var result = mapResult(inventoryCountRepository.findByStorageLocationId(locId));
if (parsedStatus != null) {
final InventoryCountStatus filterStatus = parsedStatus;
return result.map(counts -> counts.stream()
.filter(c -> c.status() == filterStatus)
.toList());
}
return result;
} }
if (parsedStatus != null) { private Result<InventoryCountError, Page<InventoryCount>> mapResult(Result<RepositoryError, Page<InventoryCount>> result) {
return mapResult(inventoryCountRepository.findByStatus(parsedStatus));
}
return mapResult(inventoryCountRepository.findAll());
}
private Result<InventoryCountError, List<InventoryCount>> mapResult(Result<RepositoryError, List<InventoryCount>> result) {
return switch (result) { return switch (result) {
case Result.Failure(var err) -> Result.failure(new InventoryCountError.RepositoryFailure(err.message())); case Result.Failure(var err) -> Result.failure(new InventoryCountError.RepositoryFailure(err.message()));
case Result.Success(var counts) -> Result.success(counts); case Result.Success(var page) -> Result.success(page);
}; };
} }
} }

View file

@ -7,13 +7,14 @@ import de.effigenix.domain.inventory.StockMovement;
import de.effigenix.domain.inventory.StockMovementError; import de.effigenix.domain.inventory.StockMovementError;
import de.effigenix.domain.inventory.StockMovementRepository; import de.effigenix.domain.inventory.StockMovementRepository;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
import java.time.Instant; import java.time.Instant;
import java.util.List;
public class ListStockMovements { public class ListStockMovements {
@ -25,81 +26,62 @@ public class ListStockMovements {
this.authPort = authPort; this.authPort = authPort;
} }
/** public Result<StockMovementError, Page<StockMovement>> execute(
* Lists stock movements with optional filtering.
* Filter priority (only one filter applied): stockId > articleId > batchReference > movementType > from/to.
* If multiple filters are provided, only the highest-priority filter is used.
*/
public Result<StockMovementError, List<StockMovement>> execute(
String stockId, String articleId, String movementType, String stockId, String articleId, String movementType,
String batchReference, Instant from, Instant to, String batchReference, Instant from, Instant to,
ActorId performedBy) { ActorId performedBy, PageRequest pageRequest) {
if (!authPort.can(performedBy, InventoryAction.STOCK_MOVEMENT_READ)) { if (!authPort.can(performedBy, InventoryAction.STOCK_MOVEMENT_READ)) {
return Result.failure(new StockMovementError.Unauthorized("Not authorized to list stock movements")); return Result.failure(new StockMovementError.Unauthorized("Not authorized to list stock movements"));
} }
// Validate filters
StockId sid = null;
if (stockId != null) { if (stockId != null) {
StockId sid;
try { try {
sid = StockId.of(stockId); sid = StockId.of(stockId);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return Result.failure(new StockMovementError.InvalidStockId(e.getMessage())); return Result.failure(new StockMovementError.InvalidStockId(e.getMessage()));
} }
return mapResult(stockMovementRepository.findAllByStockId(sid));
} }
ArticleId aid = null;
if (articleId != null) { if (articleId != null) {
ArticleId aid;
try { try {
aid = ArticleId.of(articleId); aid = ArticleId.of(articleId);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return Result.failure(new StockMovementError.InvalidArticleId(e.getMessage())); return Result.failure(new StockMovementError.InvalidArticleId(e.getMessage()));
} }
return mapResult(stockMovementRepository.findAllByArticleId(aid));
} }
if (batchReference != null) { if (batchReference != null && batchReference.isBlank()) {
if (batchReference.isBlank()) {
return Result.failure(new StockMovementError.InvalidBatchReference( return Result.failure(new StockMovementError.InvalidBatchReference(
"Batch reference must not be blank")); "Batch reference must not be blank"));
} }
return mapResult(stockMovementRepository.findAllByBatchReference(batchReference));
}
MovementType type = null;
if (movementType != null) { if (movementType != null) {
MovementType type;
try { try {
type = MovementType.valueOf(movementType); type = MovementType.valueOf(movementType);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return Result.failure(new StockMovementError.InvalidMovementType( return Result.failure(new StockMovementError.InvalidMovementType(
"Invalid movement type: " + movementType)); "Invalid movement type: " + movementType));
} }
return mapResult(stockMovementRepository.findAllByMovementType(type));
} }
if (from != null || to != null) {
if (from != null && to != null && from.isAfter(to)) { if (from != null && to != null && from.isAfter(to)) {
return Result.failure(new StockMovementError.InvalidDateRange( return Result.failure(new StockMovementError.InvalidDateRange(
"'from' must not be after 'to'")); "'from' must not be after 'to'"));
} }
if (from != null && to != null) {
return mapResult(stockMovementRepository.findAllByPerformedAtBetween(from, to)); return mapResult(stockMovementRepository.findAll(sid, aid, type, batchReference, from, to, pageRequest));
}
if (from != null) {
return mapResult(stockMovementRepository.findAllByPerformedAtAfter(from));
}
return mapResult(stockMovementRepository.findAllByPerformedAtBefore(to));
} }
return mapResult(stockMovementRepository.findAll()); private Result<StockMovementError, Page<StockMovement>> mapResult(
} Result<RepositoryError, Page<StockMovement>> result) {
private Result<StockMovementError, List<StockMovement>> mapResult(
Result<RepositoryError, List<StockMovement>> result) {
return switch (result) { return switch (result) {
case Result.Failure(var err) -> Result.failure(new StockMovementError.RepositoryFailure(err.message())); case Result.Failure(var err) -> Result.failure(new StockMovementError.RepositoryFailure(err.message()));
case Result.Success(var movements) -> Result.success(movements); case Result.Success(var page) -> Result.success(page);
}; };
} }
} }

View file

@ -5,11 +5,11 @@ import de.effigenix.domain.inventory.StockError;
import de.effigenix.domain.inventory.StockRepository; import de.effigenix.domain.inventory.StockRepository;
import de.effigenix.domain.inventory.StorageLocationId; import de.effigenix.domain.inventory.StorageLocationId;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 java.util.List;
public class ListStocks { public class ListStocks {
private final StockRepository stockRepository; private final StockRepository stockRepository;
@ -18,7 +18,7 @@ public class ListStocks {
this.stockRepository = stockRepository; this.stockRepository = stockRepository;
} }
public Result<StockError, List<Stock>> execute(String storageLocationId, String articleId) { public Result<StockError, Page<Stock>> execute(String storageLocationId, String articleId, PageRequest pageRequest) {
if (storageLocationId != null && articleId != null) { if (storageLocationId != null && articleId != null) {
return Result.failure(new StockError.InvalidFilterCombination( return Result.failure(new StockError.InvalidFilterCombination(
"Only one filter parameter allowed: storageLocationId or articleId")); "Only one filter parameter allowed: storageLocationId or articleId"));
@ -31,7 +31,7 @@ public class ListStocks {
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return Result.failure(new StockError.InvalidStorageLocationId(e.getMessage())); return Result.failure(new StockError.InvalidStorageLocationId(e.getMessage()));
} }
return mapResult(stockRepository.findAllByStorageLocationId(locId)); return mapResult(stockRepository.findAllByStorageLocationId(locId, pageRequest));
} }
if (articleId != null) { if (articleId != null) {
@ -41,16 +41,16 @@ public class ListStocks {
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return Result.failure(new StockError.InvalidArticleId(e.getMessage())); return Result.failure(new StockError.InvalidArticleId(e.getMessage()));
} }
return mapResult(stockRepository.findAllByArticleId(artId)); return mapResult(stockRepository.findAllByArticleId(artId, pageRequest));
} }
return mapResult(stockRepository.findAll()); return mapResult(stockRepository.findAll(pageRequest));
} }
private Result<StockError, List<Stock>> mapResult(Result<RepositoryError, List<Stock>> result) { private Result<StockError, Page<Stock>> mapResult(Result<RepositoryError, Page<Stock>> result) {
return switch (result) { return switch (result) {
case Result.Failure(var err) -> Result.failure(new StockError.RepositoryFailure(err.message())); case Result.Failure(var err) -> Result.failure(new StockError.RepositoryFailure(err.message()));
case Result.Success(var stocks) -> Result.success(stocks); case Result.Success(var page) -> Result.success(page);
}; };
} }
} }

View file

@ -1,11 +1,11 @@
package de.effigenix.application.inventory; package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 java.util.List;
public class ListStorageLocations { public class ListStorageLocations {
private final StorageLocationRepository storageLocationRepository; private final StorageLocationRepository storageLocationRepository;
@ -14,40 +14,24 @@ public class ListStorageLocations {
this.storageLocationRepository = storageLocationRepository; this.storageLocationRepository = storageLocationRepository;
} }
public Result<StorageLocationError, List<StorageLocation>> execute(String storageType, Boolean active) { public Result<StorageLocationError, Page<StorageLocation>> execute(String storageType, Boolean active, PageRequest pageRequest) {
StorageType type = null;
if (storageType != null) { if (storageType != null) {
return findByStorageType(storageType, active);
}
return mapResult(storageLocationRepository.findAll())
.map(locations -> filterByActive(locations, active));
}
private Result<StorageLocationError, List<StorageLocation>> findByStorageType(String storageType, Boolean active) {
StorageType type;
try { try {
type = StorageType.valueOf(storageType); type = StorageType.valueOf(storageType);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return Result.failure(new StorageLocationError.InvalidStorageType(storageType)); return Result.failure(new StorageLocationError.InvalidStorageType(storageType));
} }
return mapResult(storageLocationRepository.findByStorageType(type))
.map(locations -> filterByActive(locations, active));
} }
private List<StorageLocation> filterByActive(List<StorageLocation> locations, Boolean active) { return mapResult(storageLocationRepository.findAll(type, active, pageRequest));
if (active == null) {
return locations;
}
return locations.stream()
.filter(loc -> loc.active() == active)
.toList();
} }
private Result<StorageLocationError, List<StorageLocation>> mapResult( private Result<StorageLocationError, Page<StorageLocation>> mapResult(
Result<RepositoryError, List<StorageLocation>> result) { Result<RepositoryError, Page<StorageLocation>> result) {
return switch (result) { return switch (result) {
case Result.Failure(var err) -> Result.failure(new StorageLocationError.RepositoryFailure(err.message())); case Result.Failure(var err) -> Result.failure(new StorageLocationError.RepositoryFailure(err.message()));
case Result.Success(var locations) -> Result.success(locations); case Result.Success(var page) -> Result.success(page);
}; };
} }
} }

View file

@ -4,13 +4,10 @@ import de.effigenix.domain.masterdata.article.Article;
import de.effigenix.domain.masterdata.article.ArticleError; import de.effigenix.domain.masterdata.article.ArticleError;
import de.effigenix.domain.masterdata.article.ArticleRepository; import de.effigenix.domain.masterdata.article.ArticleRepository;
import de.effigenix.domain.masterdata.article.ArticleStatus; import de.effigenix.domain.masterdata.article.ArticleStatus;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import java.util.List;
import static de.effigenix.shared.common.Result.*;
public class ListArticles { public class ListArticles {
private final ArticleRepository articleRepository; private final ArticleRepository articleRepository;
@ -19,30 +16,12 @@ public class ListArticles {
this.articleRepository = articleRepository; this.articleRepository = articleRepository;
} }
public Result<ArticleError, List<Article>> execute() { public Result<ArticleError, Page<Article>> execute(ArticleStatus status, PageRequest pageRequest) {
return switch (articleRepository.findAll()) { return switch (articleRepository.findAll(status, pageRequest)) {
case Failure(var err) -> case Result.Failure(var err) ->
Result.failure(new ArticleError.RepositoryFailure(err.message())); Result.failure(new ArticleError.RepositoryFailure(err.message()));
case Success(var articles) -> case Result.Success(var page) ->
Result.success(articles); Result.success(page);
};
}
public Result<ArticleError, List<Article>> executeByCategory(ProductCategoryId categoryId) {
return switch (articleRepository.findByCategory(categoryId)) {
case Failure(var err) ->
Result.failure(new ArticleError.RepositoryFailure(err.message()));
case Success(var articles) ->
Result.success(articles);
};
}
public Result<ArticleError, List<Article>> executeByStatus(ArticleStatus status) {
return switch (articleRepository.findByStatus(status)) {
case Failure(var err) ->
Result.failure(new ArticleError.RepositoryFailure(err.message()));
case Success(var articles) ->
Result.success(articles);
}; };
} }
} }

View file

@ -1,12 +1,10 @@
package de.effigenix.application.masterdata.customer; package de.effigenix.application.masterdata.customer;
import de.effigenix.domain.masterdata.customer.*; import de.effigenix.domain.masterdata.customer.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import java.util.List;
import static de.effigenix.shared.common.Result.*;
public class ListCustomers { public class ListCustomers {
private final CustomerRepository customerRepository; private final CustomerRepository customerRepository;
@ -15,30 +13,12 @@ public class ListCustomers {
this.customerRepository = customerRepository; this.customerRepository = customerRepository;
} }
public Result<CustomerError, List<Customer>> execute() { public Result<CustomerError, Page<Customer>> execute(PageRequest pageRequest) {
return switch (customerRepository.findAll()) { return switch (customerRepository.findAll(pageRequest)) {
case Failure(var err) -> case Result.Failure(var err) ->
Result.failure(new CustomerError.RepositoryFailure(err.message())); Result.failure(new CustomerError.RepositoryFailure(err.message()));
case Success(var customers) -> case Result.Success(var page) ->
Result.success(customers); Result.success(page);
};
}
public Result<CustomerError, List<Customer>> executeByType(CustomerType type) {
return switch (customerRepository.findByType(type)) {
case Failure(var err) ->
Result.failure(new CustomerError.RepositoryFailure(err.message()));
case Success(var customers) ->
Result.success(customers);
};
}
public Result<CustomerError, List<Customer>> executeByStatus(CustomerStatus status) {
return switch (customerRepository.findByStatus(status)) {
case Failure(var err) ->
Result.failure(new CustomerError.RepositoryFailure(err.message()));
case Success(var customers) ->
Result.success(customers);
}; };
} }
} }

View file

@ -3,12 +3,10 @@ package de.effigenix.application.masterdata.productcategory;
import de.effigenix.domain.masterdata.productcategory.ProductCategory; import de.effigenix.domain.masterdata.productcategory.ProductCategory;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryError; import de.effigenix.domain.masterdata.productcategory.ProductCategoryError;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryRepository; import de.effigenix.domain.masterdata.productcategory.ProductCategoryRepository;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import java.util.List;
import static de.effigenix.shared.common.Result.*;
public class ListProductCategories { public class ListProductCategories {
private final ProductCategoryRepository categoryRepository; private final ProductCategoryRepository categoryRepository;
@ -17,12 +15,12 @@ public class ListProductCategories {
this.categoryRepository = categoryRepository; this.categoryRepository = categoryRepository;
} }
public Result<ProductCategoryError, List<ProductCategory>> execute() { public Result<ProductCategoryError, Page<ProductCategory>> execute(PageRequest pageRequest) {
return switch (categoryRepository.findAll()) { return switch (categoryRepository.findAll(pageRequest)) {
case Failure(var err) -> case Result.Failure(var err) ->
Result.failure(new ProductCategoryError.RepositoryFailure(err.message())); Result.failure(new ProductCategoryError.RepositoryFailure(err.message()));
case Success(var categories) -> case Result.Success(var page) ->
Result.success(categories); Result.success(page);
}; };
} }
} }

View file

@ -4,12 +4,10 @@ import de.effigenix.domain.masterdata.supplier.Supplier;
import de.effigenix.domain.masterdata.supplier.SupplierError; import de.effigenix.domain.masterdata.supplier.SupplierError;
import de.effigenix.domain.masterdata.supplier.SupplierRepository; import de.effigenix.domain.masterdata.supplier.SupplierRepository;
import de.effigenix.domain.masterdata.supplier.SupplierStatus; import de.effigenix.domain.masterdata.supplier.SupplierStatus;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import java.util.List;
import static de.effigenix.shared.common.Result.*;
public class ListSuppliers { public class ListSuppliers {
private final SupplierRepository supplierRepository; private final SupplierRepository supplierRepository;
@ -18,21 +16,12 @@ public class ListSuppliers {
this.supplierRepository = supplierRepository; this.supplierRepository = supplierRepository;
} }
public Result<SupplierError, List<Supplier>> execute() { public Result<SupplierError, Page<Supplier>> execute(SupplierStatus status, PageRequest pageRequest) {
return switch (supplierRepository.findAll()) { return switch (supplierRepository.findAll(status, pageRequest)) {
case Failure(var err) -> case Result.Failure(var err) ->
Result.failure(new SupplierError.RepositoryFailure(err.message())); Result.failure(new SupplierError.RepositoryFailure(err.message()));
case Success(var suppliers) -> case Result.Success(var page) ->
Result.success(suppliers); Result.success(page);
};
}
public Result<SupplierError, List<Supplier>> executeByStatus(SupplierStatus status) {
return switch (supplierRepository.findByStatus(status)) {
case Failure(var err) ->
Result.failure(new SupplierError.RepositoryFailure(err.message()));
case Success(var suppliers) ->
Result.success(suppliers);
}; };
} }
} }

View file

@ -1,6 +1,8 @@
package de.effigenix.application.production; package de.effigenix.application.production;
import de.effigenix.domain.production.*; import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
@ -21,16 +23,16 @@ public class ListBatches {
this.authorizationPort = authorizationPort; this.authorizationPort = authorizationPort;
} }
public Result<BatchError, List<Batch>> execute(ActorId performedBy) { public Result<BatchError, Page<Batch>> execute(ActorId performedBy, PageRequest pageRequest) {
if (!authorizationPort.can(performedBy, ProductionAction.BATCH_READ)) { if (!authorizationPort.can(performedBy, ProductionAction.BATCH_READ)) {
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches")); return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
} }
switch (batchRepository.findAllSummary()) { switch (batchRepository.findAllSummary(pageRequest)) {
case Result.Failure(var err) -> case Result.Failure(var err) ->
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); } { return Result.failure(new BatchError.RepositoryFailure(err.message())); }
case Result.Success(var batches) -> case Result.Success(var page) ->
{ return Result.success(batches); } { return Result.success(page); }
} }
} }

View file

@ -1,6 +1,8 @@
package de.effigenix.application.production; package de.effigenix.application.production;
import de.effigenix.domain.production.*; import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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.ActorId;
@ -22,12 +24,15 @@ public class ListProductionOrders {
this.authorizationPort = authorizationPort; this.authorizationPort = authorizationPort;
} }
public Result<ProductionOrderError, List<ProductionOrder>> execute(ActorId performedBy) { public Result<ProductionOrderError, Page<ProductionOrder>> execute(ActorId performedBy, PageRequest pageRequest) {
if (!authorizationPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)) { if (!authorizationPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)) {
return Result.failure(new ProductionOrderError.Unauthorized("Not authorized to list production orders")); return Result.failure(new ProductionOrderError.Unauthorized("Not authorized to list production orders"));
} }
return wrapResult(productionOrderRepository.findAll()); return switch (productionOrderRepository.findAll(pageRequest)) {
case Result.Failure(var err) -> Result.failure(new ProductionOrderError.RepositoryFailure(err.message()));
case Result.Success(var page) -> Result.success(page);
};
} }
public Result<ProductionOrderError, List<ProductionOrder>> executeByDateRange(LocalDate from, LocalDate to, ActorId performedBy) { public Result<ProductionOrderError, List<ProductionOrder>> executeByDateRange(LocalDate from, LocalDate to, ActorId performedBy) {

View file

@ -1,6 +1,8 @@
package de.effigenix.application.production; package de.effigenix.application.production;
import de.effigenix.domain.production.*; import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
@ -17,16 +19,16 @@ public class ListRecipes {
this.authorizationPort = authorizationPort; this.authorizationPort = authorizationPort;
} }
public Result<RecipeError, List<Recipe>> execute(ActorId performedBy) { public Result<RecipeError, Page<Recipe>> execute(ActorId performedBy, PageRequest pageRequest) {
if (!authorizationPort.can(performedBy, ProductionAction.RECIPE_READ)) { if (!authorizationPort.can(performedBy, ProductionAction.RECIPE_READ)) {
return Result.failure(new RecipeError.Unauthorized("Not authorized to read recipes")); return Result.failure(new RecipeError.Unauthorized("Not authorized to read recipes"));
} }
switch (recipeRepository.findAll()) { switch (recipeRepository.findAll(pageRequest)) {
case Result.Failure(var err) -> case Result.Failure(var err) ->
{ return Result.failure(new RecipeError.RepositoryFailure(err.message())); } { return Result.failure(new RecipeError.RepositoryFailure(err.message())); }
case Result.Success(var recipes) -> case Result.Success(var page) ->
{ return Result.success(recipes); } { return Result.success(page); }
} }
} }

View file

@ -2,6 +2,8 @@ package de.effigenix.application.shared;
import de.effigenix.shared.common.Country; import de.effigenix.shared.common.Country;
import de.effigenix.shared.common.CountryRepository; import de.effigenix.shared.common.CountryRepository;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import java.util.List; import java.util.List;
@ -13,7 +15,11 @@ public class ListCountries {
this.countryRepository = countryRepository; this.countryRepository = countryRepository;
} }
public List<Country> execute(String query) { public Page<Country> execute(PageRequest pageRequest) {
return countryRepository.findAll(pageRequest);
}
public List<Country> search(String query) {
if (query == null || query.isBlank()) { if (query == null || query.isBlank()) {
return countryRepository.findAll(); return countryRepository.findAll();
} }

View file

@ -4,13 +4,11 @@ import de.effigenix.application.usermanagement.dto.UserDTO;
import de.effigenix.domain.usermanagement.UserError; import de.effigenix.domain.usermanagement.UserError;
import de.effigenix.domain.usermanagement.UserManagementAction; import de.effigenix.domain.usermanagement.UserManagementAction;
import de.effigenix.domain.usermanagement.UserRepository; import de.effigenix.domain.usermanagement.UserRepository;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
import de.effigenix.shared.security.BranchId;
import java.util.List;
import java.util.stream.Collectors;
/** /**
* Use Case: List all users (with optional branch filtering). * Use Case: List all users (with optional branch filtering).
@ -25,29 +23,13 @@ public class ListUsers {
this.authPort = authPort; this.authPort = authPort;
} }
/** public Result<UserError, Page<UserDTO>> execute(String branchId, ActorId performedBy, PageRequest pageRequest) {
* Lists all users (admin view).
*/
public Result<UserError, List<UserDTO>> execute(ActorId performedBy) {
if (!authPort.can(performedBy, UserManagementAction.USER_LIST)) { if (!authPort.can(performedBy, UserManagementAction.USER_LIST)) {
return Result.failure(new UserError.Unauthorized("Not authorized to list users")); return Result.failure(new UserError.Unauthorized("Not authorized to list users"));
} }
return userRepository.findAll() return userRepository.findAll(branchId, pageRequest)
.mapError(err -> (UserError) new UserError.RepositoryFailure(err.message())) .mapError(err -> (UserError) new UserError.RepositoryFailure(err.message()))
.map(users -> users.stream().map(UserDTO::from).collect(Collectors.toList())); .map(page -> page.map(UserDTO::from));
}
/**
* Lists users for a specific branch (filtered view).
*/
public Result<UserError, List<UserDTO>> executeForBranch(BranchId branchId, ActorId performedBy) {
if (!authPort.can(performedBy, UserManagementAction.USER_LIST)) {
return Result.failure(new UserError.Unauthorized("Not authorized to list users"));
}
return userRepository.findByBranchId(branchId.value())
.mapError(err -> (UserError) new UserError.RepositoryFailure(err.message()))
.map(users -> users.stream().map(UserDTO::from).collect(Collectors.toList()));
} }
} }

View file

@ -1,5 +1,7 @@
package de.effigenix.domain.inventory; package de.effigenix.domain.inventory;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -10,6 +12,8 @@ public interface InventoryCountRepository {
Result<RepositoryError, Optional<InventoryCount>> findById(InventoryCountId id); Result<RepositoryError, Optional<InventoryCount>> findById(InventoryCountId id);
Result<RepositoryError, Page<InventoryCount>> findAll(InventoryCountStatus status, PageRequest pageRequest);
Result<RepositoryError, List<InventoryCount>> findAll(); Result<RepositoryError, List<InventoryCount>> findAll();
Result<RepositoryError, List<InventoryCount>> findByStorageLocationId(StorageLocationId storageLocationId); Result<RepositoryError, List<InventoryCount>> findByStorageLocationId(StorageLocationId storageLocationId);

View file

@ -1,6 +1,8 @@
package de.effigenix.domain.inventory; package de.effigenix.domain.inventory;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -12,6 +14,11 @@ public interface StockMovementRepository {
Result<RepositoryError, Optional<StockMovement>> findById(StockMovementId id); Result<RepositoryError, Optional<StockMovement>> findById(StockMovementId id);
Result<RepositoryError, Page<StockMovement>> findAll(StockId stockId, ArticleId articleId,
MovementType movementType, String batchRef,
Instant from, Instant to,
PageRequest pageRequest);
Result<RepositoryError, List<StockMovement>> findAll(); Result<RepositoryError, List<StockMovement>> findAll();
Result<RepositoryError, List<StockMovement>> findAllByStockId(StockId stockId); Result<RepositoryError, List<StockMovement>> findAllByStockId(StockId stockId);

View file

@ -1,6 +1,8 @@
package de.effigenix.domain.inventory; package de.effigenix.domain.inventory;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -12,6 +14,12 @@ public interface StockRepository {
Result<RepositoryError, Optional<Stock>> findById(StockId id); Result<RepositoryError, Optional<Stock>> findById(StockId id);
Result<RepositoryError, Page<Stock>> findAll(PageRequest pageRequest);
Result<RepositoryError, Page<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId, PageRequest pageRequest);
Result<RepositoryError, Page<Stock>> findAllByArticleId(ArticleId articleId, PageRequest pageRequest);
Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId); Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId);
Result<RepositoryError, Boolean> existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId); Result<RepositoryError, Boolean> existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId);
@ -26,5 +34,7 @@ public interface StockRepository {
Result<RepositoryError, List<Stock>> findAllBelowMinimumLevel(); Result<RepositoryError, List<Stock>> findAllBelowMinimumLevel();
Result<RepositoryError, List<Stock>> findAllByBatchId(String batchId);
Result<RepositoryError, Void> save(Stock stock); Result<RepositoryError, Void> save(Stock stock);
} }

View file

@ -1,5 +1,7 @@
package de.effigenix.domain.inventory; package de.effigenix.domain.inventory;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -10,6 +12,8 @@ public interface StorageLocationRepository {
Result<RepositoryError, Optional<StorageLocation>> findById(StorageLocationId id); Result<RepositoryError, Optional<StorageLocation>> findById(StorageLocationId id);
Result<RepositoryError, Page<StorageLocation>> findAll(StorageType storageType, Boolean active, PageRequest pageRequest);
Result<RepositoryError, List<StorageLocation>> findAll(); Result<RepositoryError, List<StorageLocation>> findAll();
Result<RepositoryError, List<StorageLocation>> findByStorageType(StorageType storageType); Result<RepositoryError, List<StorageLocation>> findByStorageType(StorageType storageType);

View file

@ -1,6 +1,8 @@
package de.effigenix.domain.masterdata.article; package de.effigenix.domain.masterdata.article;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -11,6 +13,8 @@ public interface ArticleRepository {
Result<RepositoryError, Optional<Article>> findById(ArticleId id); Result<RepositoryError, Optional<Article>> findById(ArticleId id);
Result<RepositoryError, Page<Article>> findAll(ArticleStatus status, PageRequest pageRequest);
Result<RepositoryError, List<Article>> findAll(); Result<RepositoryError, List<Article>> findAll();
Result<RepositoryError, List<Article>> findByCategory(ProductCategoryId categoryId); Result<RepositoryError, List<Article>> findByCategory(ProductCategoryId categoryId);

View file

@ -1,5 +1,7 @@
package de.effigenix.domain.masterdata.customer; package de.effigenix.domain.masterdata.customer;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -10,6 +12,8 @@ public interface CustomerRepository {
Result<RepositoryError, Optional<Customer>> findById(CustomerId id); Result<RepositoryError, Optional<Customer>> findById(CustomerId id);
Result<RepositoryError, Page<Customer>> findAll(PageRequest pageRequest);
Result<RepositoryError, List<Customer>> findAll(); Result<RepositoryError, List<Customer>> findAll();
Result<RepositoryError, List<Customer>> findByType(CustomerType type); Result<RepositoryError, List<Customer>> findByType(CustomerType type);

View file

@ -1,5 +1,7 @@
package de.effigenix.domain.masterdata.productcategory; package de.effigenix.domain.masterdata.productcategory;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -10,6 +12,8 @@ public interface ProductCategoryRepository {
Result<RepositoryError, Optional<ProductCategory>> findById(ProductCategoryId id); Result<RepositoryError, Optional<ProductCategory>> findById(ProductCategoryId id);
Result<RepositoryError, Page<ProductCategory>> findAll(PageRequest pageRequest);
Result<RepositoryError, List<ProductCategory>> findAll(); Result<RepositoryError, List<ProductCategory>> findAll();
Result<RepositoryError, Void> save(ProductCategory category); Result<RepositoryError, Void> save(ProductCategory category);

View file

@ -1,5 +1,7 @@
package de.effigenix.domain.masterdata.supplier; package de.effigenix.domain.masterdata.supplier;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -10,6 +12,8 @@ public interface SupplierRepository {
Result<RepositoryError, Optional<Supplier>> findById(SupplierId id); Result<RepositoryError, Optional<Supplier>> findById(SupplierId id);
Result<RepositoryError, Page<Supplier>> findAll(SupplierStatus status, PageRequest pageRequest);
Result<RepositoryError, List<Supplier>> findAll(); Result<RepositoryError, List<Supplier>> findAll();
Result<RepositoryError, List<Supplier>> findByStatus(SupplierStatus status); Result<RepositoryError, List<Supplier>> findByStatus(SupplierStatus status);

View file

@ -1,5 +1,7 @@
package de.effigenix.domain.production; package de.effigenix.domain.production;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -11,6 +13,8 @@ public interface BatchRepository {
Result<RepositoryError, Optional<Batch>> findById(BatchId id); Result<RepositoryError, Optional<Batch>> findById(BatchId id);
Result<RepositoryError, Page<Batch>> findAllSummary(PageRequest pageRequest);
Result<RepositoryError, List<Batch>> findAll(); Result<RepositoryError, List<Batch>> findAll();
Result<RepositoryError, Optional<Batch>> findByBatchNumber(BatchNumber batchNumber); Result<RepositoryError, Optional<Batch>> findByBatchNumber(BatchNumber batchNumber);

View file

@ -1,5 +1,7 @@
package de.effigenix.domain.production; package de.effigenix.domain.production;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -11,6 +13,8 @@ public interface ProductionOrderRepository {
Result<RepositoryError, Optional<ProductionOrder>> findById(ProductionOrderId id); Result<RepositoryError, Optional<ProductionOrder>> findById(ProductionOrderId id);
Result<RepositoryError, Page<ProductionOrder>> findAll(PageRequest pageRequest);
Result<RepositoryError, List<ProductionOrder>> findAll(); Result<RepositoryError, List<ProductionOrder>> findAll();
Result<RepositoryError, List<ProductionOrder>> findByDateRange(LocalDate from, LocalDate to); Result<RepositoryError, List<ProductionOrder>> findByDateRange(LocalDate from, LocalDate to);

View file

@ -1,5 +1,7 @@
package de.effigenix.domain.production; package de.effigenix.domain.production;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -10,6 +12,8 @@ public interface RecipeRepository {
Result<RepositoryError, Optional<Recipe>> findById(RecipeId id); Result<RepositoryError, Optional<Recipe>> findById(RecipeId id);
Result<RepositoryError, Page<Recipe>> findAll(PageRequest pageRequest);
Result<RepositoryError, List<Recipe>> findAll(); Result<RepositoryError, List<Recipe>> findAll();
Result<RepositoryError, Void> save(Recipe recipe); Result<RepositoryError, Void> save(Recipe recipe);

View file

@ -1,5 +1,7 @@
package de.effigenix.domain.usermanagement; package de.effigenix.domain.usermanagement;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -14,6 +16,8 @@ public interface RoleRepository {
Result<RepositoryError, Optional<Role>> findById(RoleId id); Result<RepositoryError, Optional<Role>> findById(RoleId id);
Result<RepositoryError, Page<Role>> findAll(PageRequest pageRequest);
Result<RepositoryError, Optional<Role>> findByName(RoleName name); Result<RepositoryError, Optional<Role>> findByName(RoleName name);
Result<RepositoryError, List<Role>> findAll(); Result<RepositoryError, List<Role>> findAll();

View file

@ -1,5 +1,7 @@
package de.effigenix.domain.usermanagement; package de.effigenix.domain.usermanagement;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -14,6 +16,8 @@ public interface UserRepository {
Result<RepositoryError, Optional<User>> findById(UserId id); Result<RepositoryError, Optional<User>> findById(UserId id);
Result<RepositoryError, Page<User>> findAll(String branchId, PageRequest pageRequest);
Result<RepositoryError, Optional<User>> findByUsername(String username); Result<RepositoryError, Optional<User>> findByUsername(String username);
Result<RepositoryError, Optional<User>> findByEmail(String email); Result<RepositoryError, Optional<User>> findByEmail(String email);

View file

@ -2,6 +2,9 @@ package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -19,6 +22,7 @@ import java.time.LocalDate;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
@ -27,6 +31,12 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcInventoryCountRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcInventoryCountRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"createdAt", "created_at",
"status", "status",
"countDate", "count_date"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcInventoryCountRepository(JdbcClient jdbc) { public JdbcInventoryCountRepository(JdbcClient jdbc) {
@ -50,6 +60,33 @@ public class JdbcInventoryCountRepository implements InventoryCountRepository {
} }
} }
@Override
public Result<RepositoryError, Page<InventoryCount>> findAll(InventoryCountStatus status, PageRequest pageRequest) {
try {
String where = "";
var params = new java.util.LinkedHashMap<String, Object>();
if (status != null) {
where = " WHERE status = :status";
params.put("status", status.name());
}
long total = jdbc.sql("SELECT COUNT(*) FROM inventory_counts" + where)
.params(params)
.query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY created_at DESC";
params.put("limit", pageRequest.size());
params.put("offset", pageRequest.offset());
var counts = jdbc.sql("SELECT * FROM inventory_counts" + where + orderBy + " LIMIT :limit OFFSET :offset")
.params(params)
.query(this::mapCountRow)
.list();
return Result.success(Page.of(loadChildrenForAll(counts), pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.warn("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<InventoryCount>> findAll() { public Result<RepositoryError, List<InventoryCount>> findAll() {
try { try {

View file

@ -2,6 +2,9 @@ package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -18,6 +21,7 @@ import java.time.Instant;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
@ -26,6 +30,12 @@ public class JdbcStockMovementRepository implements StockMovementRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcStockMovementRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcStockMovementRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"performedAt", "performed_at",
"movementType", "movement_type",
"articleId", "article_id"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcStockMovementRepository(JdbcClient jdbc) { public JdbcStockMovementRepository(JdbcClient jdbc) {
@ -46,6 +56,61 @@ public class JdbcStockMovementRepository implements StockMovementRepository {
} }
} }
@Override
public Result<RepositoryError, Page<StockMovement>> findAll(StockId stockId, ArticleId articleId,
MovementType movementType, String batchRef,
Instant from, Instant to,
PageRequest pageRequest) {
try {
var where = new StringBuilder();
var params = new java.util.LinkedHashMap<String, Object>();
if (stockId != null) {
where.append(" WHERE stock_id = :stockId");
params.put("stockId", stockId.value());
}
if (articleId != null) {
where.append(where.isEmpty() ? " WHERE " : " AND ");
where.append("article_id = :articleId");
params.put("articleId", articleId.value());
}
if (movementType != null) {
where.append(where.isEmpty() ? " WHERE " : " AND ");
where.append("movement_type = :movementType");
params.put("movementType", movementType.name());
}
if (batchRef != null) {
where.append(where.isEmpty() ? " WHERE " : " AND ");
where.append("batch_id = :batchRef");
params.put("batchRef", batchRef);
}
if (from != null) {
where.append(where.isEmpty() ? " WHERE " : " AND ");
where.append("performed_at >= :from");
params.put("from", from.atOffset(java.time.ZoneOffset.UTC));
}
if (to != null) {
where.append(where.isEmpty() ? " WHERE " : " AND ");
where.append("performed_at <= :to");
params.put("to", to.atOffset(java.time.ZoneOffset.UTC));
}
long total = jdbc.sql("SELECT COUNT(*) FROM stock_movements" + where)
.params(params)
.query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY performed_at DESC";
params.put("limit", pageRequest.size());
params.put("offset", pageRequest.offset());
var items = jdbc.sql("SELECT * FROM stock_movements" + where + orderBy + " LIMIT :limit OFFSET :offset")
.params(params)
.query(this::mapRow)
.list();
return Result.success(Page.of(items, pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<StockMovement>> findAll() { public Result<RepositoryError, List<StockMovement>> findAll() {
try { try {

View file

@ -2,6 +2,9 @@ package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -20,6 +23,7 @@ import java.time.OffsetDateTime;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
@ -28,6 +32,13 @@ public class JdbcStockRepository implements StockRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcStockRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcStockRepository.class);
private static final int SAFETY_LIMIT = 500;
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"articleId", "article_id",
"storageLocationId", "storage_location_id"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcStockRepository(JdbcClient jdbc) { public JdbcStockRepository(JdbcClient jdbc) {
@ -51,6 +62,63 @@ public class JdbcStockRepository implements StockRepository {
} }
} }
@Override
public Result<RepositoryError, Page<Stock>> findAll(PageRequest pageRequest) {
try {
long total = jdbc.sql("SELECT COUNT(*) FROM stocks").query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
var stocks = jdbc.sql("SELECT * FROM stocks" + orderBy + " LIMIT :limit OFFSET :offset")
.param("limit", pageRequest.size())
.param("offset", pageRequest.offset())
.query(this::mapStockRow)
.list();
return Result.success(Page.of(loadChildrenForAll(stocks), pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Page<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId, PageRequest pageRequest) {
try {
long total = jdbc.sql("SELECT COUNT(*) FROM stocks WHERE storage_location_id = :storageLocationId")
.param("storageLocationId", storageLocationId.value())
.query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
var stocks = jdbc.sql("SELECT * FROM stocks WHERE storage_location_id = :storageLocationId" + orderBy + " LIMIT :limit OFFSET :offset")
.param("storageLocationId", storageLocationId.value())
.param("limit", pageRequest.size())
.param("offset", pageRequest.offset())
.query(this::mapStockRow)
.list();
return Result.success(Page.of(loadChildrenForAll(stocks), pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAllByStorageLocationId(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, Page<Stock>> findAllByArticleId(ArticleId articleId, PageRequest pageRequest) {
try {
long total = jdbc.sql("SELECT COUNT(*) FROM stocks WHERE article_id = :articleId")
.param("articleId", articleId.value())
.query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
var stocks = jdbc.sql("SELECT * FROM stocks WHERE article_id = :articleId" + orderBy + " LIMIT :limit OFFSET :offset")
.param("articleId", articleId.value())
.param("limit", pageRequest.size())
.param("offset", pageRequest.offset())
.query(this::mapStockRow)
.list();
return Result.success(Page.of(loadChildrenForAll(stocks), pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAllByArticleId(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) {
try { try {
@ -129,18 +197,24 @@ public class JdbcStockRepository implements StockRepository {
@Override @Override
public Result<RepositoryError, List<Stock>> findAllWithExpiryRelevantBatches(LocalDate referenceDate) { public Result<RepositoryError, List<Stock>> findAllWithExpiryRelevantBatches(LocalDate referenceDate) {
try { try {
var stocks = jdbc.sql(""" var items = new ArrayList<>(jdbc.sql("""
SELECT DISTINCT s.* FROM stocks s SELECT DISTINCT s.* FROM stocks s
JOIN stock_batches b ON b.stock_id = s.id JOIN stock_batches b ON b.stock_id = s.id
WHERE (b.status IN ('AVAILABLE', 'EXPIRING_SOON') AND b.expiry_date < :today) WHERE (b.status IN ('AVAILABLE', 'EXPIRING_SOON') AND b.expiry_date < :today)
OR (s.minimum_shelf_life_days IS NOT NULL AND b.status = 'AVAILABLE' OR (s.minimum_shelf_life_days IS NOT NULL AND b.status = 'AVAILABLE'
AND b.expiry_date >= :today AND b.expiry_date >= :today
AND b.expiry_date < :today + s.minimum_shelf_life_days * INTERVAL '1 day') AND b.expiry_date < :today + s.minimum_shelf_life_days * INTERVAL '1 day')
LIMIT :limit
""") """)
.param("today", referenceDate) .param("today", referenceDate)
.param("limit", SAFETY_LIMIT + 1)
.query(this::mapStockRow) .query(this::mapStockRow)
.list(); .list());
return Result.success(loadChildrenForAll(stocks)); if (items.size() > SAFETY_LIMIT) {
logger.warn("Safety limit reached for findAllWithExpiryRelevantBatches: {} results (limit: {})", items.size(), SAFETY_LIMIT);
items = new ArrayList<>(items.subList(0, SAFETY_LIMIT));
}
return Result.success(loadChildrenForAll(items));
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findAllWithExpiryRelevantBatches", e); logger.trace("Database error in findAllWithExpiryRelevantBatches", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
@ -150,18 +224,42 @@ public class JdbcStockRepository implements StockRepository {
@Override @Override
public Result<RepositoryError, List<Stock>> findAllBelowMinimumLevel() { public Result<RepositoryError, List<Stock>> findAllBelowMinimumLevel() {
try { try {
var stocks = jdbc.sql(""" var items = new ArrayList<>(jdbc.sql("""
SELECT DISTINCT s.* FROM stocks s SELECT DISTINCT s.* FROM stocks s
LEFT JOIN stock_batches b ON b.stock_id = s.id AND b.status IN ('AVAILABLE', 'EXPIRING_SOON') LEFT JOIN stock_batches b ON b.stock_id = s.id AND b.status IN ('AVAILABLE', 'EXPIRING_SOON')
WHERE s.minimum_level_amount IS NOT NULL WHERE s.minimum_level_amount IS NOT NULL
GROUP BY s.id GROUP BY s.id
HAVING COALESCE(SUM(b.quantity_amount), 0) < s.minimum_level_amount HAVING COALESCE(SUM(b.quantity_amount), 0) < s.minimum_level_amount
LIMIT :limit
""") """)
.param("limit", SAFETY_LIMIT + 1)
.query(this::mapStockRow)
.list());
if (items.size() > SAFETY_LIMIT) {
logger.warn("Safety limit reached for findAllBelowMinimumLevel: {} results (limit: {})", items.size(), SAFETY_LIMIT);
items = new ArrayList<>(items.subList(0, SAFETY_LIMIT));
}
return Result.success(loadChildrenForAll(items));
} catch (Exception e) {
logger.trace("Database error in findAllBelowMinimumLevel", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Stock>> findAllByBatchId(String batchId) {
try {
var stocks = jdbc.sql("""
SELECT DISTINCT s.* FROM stocks s
JOIN stock_batches b ON b.stock_id = s.id
WHERE b.batch_id = :batchId
""")
.param("batchId", batchId)
.query(this::mapStockRow) .query(this::mapStockRow)
.list(); .list();
return Result.success(loadChildrenForAll(stocks)); return Result.success(loadChildrenForAll(stocks));
} catch (Exception e) { } catch (Exception e) {
logger.trace("Database error in findAllBelowMinimumLevel", e); logger.trace("Database error in findAllByBatchId", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage())); return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
} }
} }

View file

@ -1,6 +1,9 @@
package de.effigenix.infrastructure.inventory.persistence.repository; package de.effigenix.infrastructure.inventory.persistence.repository;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.slf4j.Logger; import org.slf4j.Logger;
@ -13,6 +16,7 @@ import java.math.BigDecimal;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
@ -21,6 +25,12 @@ public class JdbcStorageLocationRepository implements StorageLocationRepository
private static final Logger logger = LoggerFactory.getLogger(JdbcStorageLocationRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcStorageLocationRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"name", "name",
"storageType", "storage_type",
"active", "active"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcStorageLocationRepository(JdbcClient jdbc) { public JdbcStorageLocationRepository(JdbcClient jdbc) {
@ -41,6 +51,38 @@ public class JdbcStorageLocationRepository implements StorageLocationRepository
} }
} }
@Override
public Result<RepositoryError, Page<StorageLocation>> findAll(StorageType storageType, Boolean active, PageRequest pageRequest) {
try {
var where = new StringBuilder();
var params = new java.util.LinkedHashMap<String, Object>();
if (storageType != null) {
where.append(" WHERE storage_type = :storageType");
params.put("storageType", storageType.name());
}
if (active != null) {
where.append(where.isEmpty() ? " WHERE " : " AND ");
where.append("active = :active");
params.put("active", active);
}
long total = jdbc.sql("SELECT COUNT(*) FROM storage_locations" + where)
.params(params)
.query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY name";
params.put("limit", pageRequest.size());
params.put("offset", pageRequest.offset());
var items = jdbc.sql("SELECT * FROM storage_locations" + where + orderBy + " LIMIT :limit OFFSET :offset")
.params(params)
.query(this::mapRow)
.list();
return Result.success(Page.of(items, pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<StorageLocation>> findAll() { public Result<RepositoryError, List<StorageLocation>> findAll() {
try { try {

View file

@ -16,7 +16,10 @@ import de.effigenix.infrastructure.inventory.web.dto.CancelInventoryCountRequest
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.infrastructure.inventory.web.dto.RecordCountItemRequest; import de.effigenix.infrastructure.inventory.web.dto.RecordCountItemRequest;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.UserLookupPort; import de.effigenix.shared.security.UserLookupPort;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@ -96,19 +99,19 @@ public class InventoryCountController {
@GetMapping @GetMapping
@PreAuthorize("hasAuthority('INVENTORY_COUNT_READ')") @PreAuthorize("hasAuthority('INVENTORY_COUNT_READ')")
public ResponseEntity<List<InventoryCountResponse>> listInventoryCounts( public ResponseEntity<PageResponse<InventoryCountResponse>> listInventoryCounts(
@RequestParam(required = false) String storageLocationId,
@RequestParam(required = false) String status, @RequestParam(required = false) String status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort,
Authentication authentication Authentication authentication
) { ) {
return switch (listInventoryCounts.execute(storageLocationId, status, ActorId.of(authentication.getName()))) { var pageRequest = PageRequest.of(page, Math.min(size, 100),
sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
return switch (listInventoryCounts.execute(status, ActorId.of(authentication.getName()), pageRequest)) {
case Result.Failure(var err) -> throw new InventoryCountDomainErrorException(err); case Result.Failure(var err) -> throw new InventoryCountDomainErrorException(err);
case Result.Success(var counts) -> { case Result.Success(var countPage) -> ResponseEntity.ok(
var responses = counts.stream() PageResponse.from(countPage, c -> InventoryCountResponse.from(c, userLookup)));
.map(c -> InventoryCountResponse.from(c, userLookup))
.toList();
yield ResponseEntity.ok(responses);
}
}; };
} }

View file

@ -22,7 +22,9 @@ import de.effigenix.application.inventory.command.ReserveStockCommand;
import de.effigenix.application.inventory.command.UnblockStockBatchCommand; import de.effigenix.application.inventory.command.UnblockStockBatchCommand;
import de.effigenix.application.inventory.command.UpdateStockCommand; import de.effigenix.application.inventory.command.UpdateStockCommand;
import de.effigenix.domain.inventory.StockError; import de.effigenix.domain.inventory.StockError;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.infrastructure.inventory.web.dto.AddStockBatchRequest; import de.effigenix.infrastructure.inventory.web.dto.AddStockBatchRequest;
import de.effigenix.infrastructure.inventory.web.dto.BlockStockBatchRequest; import de.effigenix.infrastructure.inventory.web.dto.BlockStockBatchRequest;
@ -33,6 +35,7 @@ import de.effigenix.infrastructure.inventory.web.dto.ReservationResponse;
import de.effigenix.infrastructure.inventory.web.dto.ReserveStockRequest; import de.effigenix.infrastructure.inventory.web.dto.ReserveStockRequest;
import de.effigenix.infrastructure.inventory.web.dto.StockBatchResponse; import de.effigenix.infrastructure.inventory.web.dto.StockBatchResponse;
import de.effigenix.infrastructure.inventory.web.dto.StockResponse; import de.effigenix.infrastructure.inventory.web.dto.StockResponse;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.infrastructure.inventory.web.dto.UpdateStockRequest; import de.effigenix.infrastructure.inventory.web.dto.UpdateStockRequest;
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;
@ -90,18 +93,19 @@ public class StockController {
@GetMapping @GetMapping
@PreAuthorize("hasAuthority('STOCK_READ')") @PreAuthorize("hasAuthority('STOCK_READ')")
public ResponseEntity<List<StockResponse>> listStocks( public ResponseEntity<PageResponse<StockResponse>> listStocks(
@RequestParam(required = false) String storageLocationId, @RequestParam(required = false) String storageLocationId,
@RequestParam(required = false) String articleId @RequestParam(required = false) String articleId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort
) { ) {
return switch (listStocks.execute(storageLocationId, articleId)) { var pageRequest = PageRequest.of(page, Math.min(size, 100),
sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
return switch (listStocks.execute(storageLocationId, articleId, pageRequest)) {
case Result.Failure(var err) -> throw new StockDomainErrorException(err); case Result.Failure(var err) -> throw new StockDomainErrorException(err);
case Result.Success(var stocks) -> { case Result.Success(var stockPage) -> ResponseEntity.ok(
var responses = stocks.stream() PageResponse.from(stockPage, StockResponse::from));
.map(StockResponse::from)
.toList();
yield ResponseEntity.ok(responses);
}
}; };
} }

View file

@ -7,7 +7,10 @@ import de.effigenix.application.inventory.command.RecordStockMovementCommand;
import de.effigenix.domain.inventory.StockMovementError; import de.effigenix.domain.inventory.StockMovementError;
import de.effigenix.infrastructure.inventory.web.dto.RecordStockMovementRequest; import de.effigenix.infrastructure.inventory.web.dto.RecordStockMovementRequest;
import de.effigenix.infrastructure.inventory.web.dto.StockMovementResponse; import de.effigenix.infrastructure.inventory.web.dto.StockMovementResponse;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@ -76,24 +79,25 @@ public class StockMovementController {
@PreAuthorize("hasAuthority('STOCK_MOVEMENT_READ')") @PreAuthorize("hasAuthority('STOCK_MOVEMENT_READ')")
@Operation(summary = "List stock movements", @Operation(summary = "List stock movements",
description = "Filter priority (only one filter applied): stockId > articleId > batchReference > movementType > from/to") description = "Filter priority (only one filter applied): stockId > articleId > batchReference > movementType > from/to")
public ResponseEntity<List<StockMovementResponse>> listMovements( public ResponseEntity<PageResponse<StockMovementResponse>> listMovements(
@RequestParam(required = false) String stockId, @RequestParam(required = false) String stockId,
@RequestParam(required = false) String articleId, @RequestParam(required = false) String articleId,
@RequestParam(required = false) String movementType, @RequestParam(required = false) String movementType,
@RequestParam(required = false) String batchReference, @RequestParam(required = false) String batchReference,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant from, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant from,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant to, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant to,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort,
Authentication authentication Authentication authentication
) { ) {
var pageRequest = PageRequest.of(page, Math.min(size, 100),
sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
return switch (listStockMovements.execute(stockId, articleId, movementType, return switch (listStockMovements.execute(stockId, articleId, movementType,
batchReference, from, to, ActorId.of(authentication.getName()))) { batchReference, from, to, ActorId.of(authentication.getName()), pageRequest)) {
case Result.Failure(var err) -> throw new StockMovementDomainErrorException(err); case Result.Failure(var err) -> throw new StockMovementDomainErrorException(err);
case Result.Success(var movements) -> { case Result.Success(var movementPage) -> ResponseEntity.ok(
var responses = movements.stream() PageResponse.from(movementPage, StockMovementResponse::from));
.map(StockMovementResponse::from)
.toList();
yield ResponseEntity.ok(responses);
}
}; };
} }

View file

@ -12,7 +12,10 @@ import de.effigenix.domain.inventory.StorageLocationError;
import de.effigenix.infrastructure.inventory.web.dto.CreateStorageLocationRequest; import de.effigenix.infrastructure.inventory.web.dto.CreateStorageLocationRequest;
import de.effigenix.infrastructure.inventory.web.dto.StorageLocationResponse; import de.effigenix.infrastructure.inventory.web.dto.StorageLocationResponse;
import de.effigenix.infrastructure.inventory.web.dto.UpdateStorageLocationRequest; import de.effigenix.infrastructure.inventory.web.dto.UpdateStorageLocationRequest;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; 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;
@ -60,18 +63,19 @@ public class StorageLocationController {
@GetMapping @GetMapping
@PreAuthorize("hasAuthority('STOCK_READ') or hasAuthority('STOCK_WRITE')") @PreAuthorize("hasAuthority('STOCK_READ') or hasAuthority('STOCK_WRITE')")
public ResponseEntity<List<StorageLocationResponse>> listStorageLocations( public ResponseEntity<PageResponse<StorageLocationResponse>> listStorageLocations(
@RequestParam(value = "storageType", required = false) String storageType, @RequestParam(value = "storageType", required = false) String storageType,
@RequestParam(value = "active", required = false) Boolean active @RequestParam(value = "active", required = false) Boolean active,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort
) { ) {
return switch (listStorageLocations.execute(storageType, active)) { var pageRequest = PageRequest.of(page, Math.min(size, 100),
sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
return switch (listStorageLocations.execute(storageType, active, pageRequest)) {
case Result.Failure(var err) -> throw new StorageLocationDomainErrorException(err); case Result.Failure(var err) -> throw new StorageLocationDomainErrorException(err);
case Result.Success(var locations) -> { case Result.Success(var locationPage) -> ResponseEntity.ok(
var response = locations.stream() PageResponse.from(locationPage, StorageLocationResponse::from));
.map(StorageLocationResponse::from)
.toList();
yield ResponseEntity.ok(response);
}
}; };
} }

View file

@ -4,9 +4,8 @@ import de.effigenix.domain.masterdata.*;
import de.effigenix.domain.masterdata.article.*; import de.effigenix.domain.masterdata.article.*;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId;
import de.effigenix.domain.masterdata.supplier.SupplierId; import de.effigenix.domain.masterdata.supplier.SupplierId;
import de.effigenix.shared.common.Money; import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.*;
import de.effigenix.shared.common.Result;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -24,6 +23,13 @@ public class JdbcArticleRepository implements ArticleRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcArticleRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcArticleRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"name", "name",
"articleNumber", "article_number",
"status", "status",
"createdAt", "created_at"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcArticleRepository(JdbcClient jdbc) { public JdbcArticleRepository(JdbcClient jdbc) {
@ -47,6 +53,31 @@ public class JdbcArticleRepository implements ArticleRepository {
} }
} }
@Override
public Result<RepositoryError, Page<Article>> findAll(ArticleStatus status, PageRequest pageRequest) {
try {
String where = "";
var params = new LinkedHashMap<String, Object>();
if (status != null) {
where = " WHERE status = :status";
params.put("status", status.name());
}
long total = jdbc.sql("SELECT COUNT(*) FROM articles" + where)
.params(params).query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY name";
params.put("limit", pageRequest.size());
params.put("offset", pageRequest.offset());
var articles = jdbc.sql("SELECT * FROM articles" + where + orderBy + " LIMIT :limit OFFSET :offset")
.params(params).query(this::mapArticleRow).list()
.stream().map(this::loadChildren).toList();
return Result.success(Page.of(articles, pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<Article>> findAll() { public Result<RepositoryError, List<Article>> findAll() {
try { try {

View file

@ -3,6 +3,7 @@ package de.effigenix.infrastructure.masterdata.persistence;
import de.effigenix.domain.masterdata.*; import de.effigenix.domain.masterdata.*;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.domain.masterdata.customer.*; import de.effigenix.domain.masterdata.customer.*;
import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.*; import de.effigenix.shared.common.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -23,6 +24,13 @@ public class JdbcCustomerRepository implements CustomerRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcCustomerRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcCustomerRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"name", "name",
"type", "type",
"status", "status",
"createdAt", "created_at"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcCustomerRepository(JdbcClient jdbc) { public JdbcCustomerRepository(JdbcClient jdbc) {
@ -46,6 +54,24 @@ public class JdbcCustomerRepository implements CustomerRepository {
} }
} }
@Override
public Result<RepositoryError, Page<Customer>> findAll(PageRequest pageRequest) {
try {
long total = jdbc.sql("SELECT COUNT(*) FROM customers").query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY name";
var customers = jdbc.sql("SELECT * FROM customers" + orderBy + " LIMIT :limit OFFSET :offset")
.param("limit", pageRequest.size())
.param("offset", pageRequest.offset())
.query(this::mapCustomerRow).list()
.stream().map(this::loadChildren).toList();
return Result.success(Page.of(customers, pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<Customer>> findAll() { public Result<RepositoryError, List<Customer>> findAll() {
try { try {

View file

@ -4,8 +4,8 @@ import de.effigenix.domain.masterdata.productcategory.CategoryName;
import de.effigenix.domain.masterdata.productcategory.ProductCategory; import de.effigenix.domain.masterdata.productcategory.ProductCategory;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryRepository; import de.effigenix.domain.masterdata.productcategory.ProductCategoryRepository;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -15,6 +15,7 @@ import org.springframework.stereotype.Repository;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
@ -23,6 +24,10 @@ public class JdbcProductCategoryRepository implements ProductCategoryRepository
private static final Logger logger = LoggerFactory.getLogger(JdbcProductCategoryRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcProductCategoryRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"name", "name"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcProductCategoryRepository(JdbcClient jdbc) { public JdbcProductCategoryRepository(JdbcClient jdbc) {
@ -43,6 +48,23 @@ public class JdbcProductCategoryRepository implements ProductCategoryRepository
} }
} }
@Override
public Result<RepositoryError, Page<ProductCategory>> findAll(PageRequest pageRequest) {
try {
long total = jdbc.sql("SELECT COUNT(*) FROM product_categories").query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY name";
var categories = jdbc.sql("SELECT * FROM product_categories" + orderBy + " LIMIT :limit OFFSET :offset")
.param("limit", pageRequest.size())
.param("offset", pageRequest.offset())
.query(this::mapRow).list();
return Result.success(Page.of(categories, pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<ProductCategory>> findAll() { public Result<RepositoryError, List<ProductCategory>> findAll() {
try { try {

View file

@ -1,6 +1,7 @@
package de.effigenix.infrastructure.masterdata.persistence; package de.effigenix.infrastructure.masterdata.persistence;
import de.effigenix.domain.masterdata.supplier.*; import de.effigenix.domain.masterdata.supplier.*;
import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.*; import de.effigenix.shared.common.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -12,7 +13,9 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
@ -21,6 +24,12 @@ public class JdbcSupplierRepository implements SupplierRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcSupplierRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcSupplierRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"name", "name",
"status", "status",
"createdAt", "created_at"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcSupplierRepository(JdbcClient jdbc) { public JdbcSupplierRepository(JdbcClient jdbc) {
@ -44,6 +53,31 @@ public class JdbcSupplierRepository implements SupplierRepository {
} }
} }
@Override
public Result<RepositoryError, Page<Supplier>> findAll(SupplierStatus status, PageRequest pageRequest) {
try {
String where = "";
var params = new LinkedHashMap<String, Object>();
if (status != null) {
where = " WHERE status = :status";
params.put("status", status.name());
}
long total = jdbc.sql("SELECT COUNT(*) FROM suppliers" + where)
.params(params).query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY name";
params.put("limit", pageRequest.size());
params.put("offset", pageRequest.offset());
var suppliers = jdbc.sql("SELECT * FROM suppliers" + where + orderBy + " LIMIT :limit OFFSET :offset")
.params(params).query(this::mapSupplierRow).list()
.stream().map(this::loadCertificates).toList();
return Result.success(Page.of(suppliers, pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<Supplier>> findAll() { public Result<RepositoryError, List<Supplier>> findAll() {
try { try {

View file

@ -6,13 +6,14 @@ import de.effigenix.application.masterdata.article.AssignSupplier;
import de.effigenix.application.masterdata.supplier.RemoveSupplier; import de.effigenix.application.masterdata.supplier.RemoveSupplier;
import de.effigenix.application.masterdata.article.command.AssignSupplierCommand; import de.effigenix.application.masterdata.article.command.AssignSupplierCommand;
import de.effigenix.application.masterdata.supplier.command.RemoveSupplierCommand; import de.effigenix.application.masterdata.supplier.command.RemoveSupplierCommand;
import de.effigenix.domain.masterdata.article.Article;
import de.effigenix.domain.masterdata.article.ArticleError; import de.effigenix.domain.masterdata.article.ArticleError;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.domain.masterdata.article.ArticleStatus; import de.effigenix.domain.masterdata.article.ArticleStatus;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryId;
import de.effigenix.infrastructure.masterdata.web.dto.*; import de.effigenix.infrastructure.masterdata.web.dto.*;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; 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;
@ -97,29 +98,25 @@ public class ArticleController {
} }
@GetMapping @GetMapping
public ResponseEntity<List<ArticleResponse>> listArticles( public ResponseEntity<PageResponse<ArticleResponse>> listArticles(
@RequestParam(value = "categoryId", required = false) String categoryId,
@RequestParam(value = "status", required = false) ArticleStatus status, @RequestParam(value = "status", required = false) ArticleStatus status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort,
Authentication authentication Authentication authentication
) { ) {
var actorId = extractActorId(authentication); var actorId = extractActorId(authentication);
logger.info("Listing articles by actor: {}", actorId.value()); logger.info("Listing articles by actor: {}", actorId.value());
Result<ArticleError, List<Article>> result; var pageRequest = PageRequest.of(page, Math.min(size, 100),
if (categoryId != null) { sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
result = listArticles.executeByCategory(ProductCategoryId.of(categoryId)); var result = listArticles.execute(status, pageRequest);
} else if (status != null) {
result = listArticles.executeByStatus(status);
} else {
result = listArticles.execute();
}
if (result.isFailure()) { if (result.isFailure()) {
throw new ArticleDomainErrorException(result.unsafeGetError()); throw new ArticleDomainErrorException(result.unsafeGetError());
} }
var response = result.unsafeGetValue().stream().map(ArticleResponse::from).toList(); return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), ArticleResponse::from));
return ResponseEntity.ok(response);
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View file

@ -2,13 +2,13 @@ package de.effigenix.infrastructure.masterdata.web.controller;
import de.effigenix.application.masterdata.customer.*; import de.effigenix.application.masterdata.customer.*;
import de.effigenix.application.masterdata.customer.command.*; import de.effigenix.application.masterdata.customer.command.*;
import de.effigenix.domain.masterdata.customer.Customer;
import de.effigenix.domain.masterdata.customer.CustomerError; import de.effigenix.domain.masterdata.customer.CustomerError;
import de.effigenix.domain.masterdata.customer.CustomerId; import de.effigenix.domain.masterdata.customer.CustomerId;
import de.effigenix.domain.masterdata.customer.CustomerStatus;
import de.effigenix.domain.masterdata.customer.CustomerType;
import de.effigenix.infrastructure.masterdata.web.dto.*; import de.effigenix.infrastructure.masterdata.web.dto.*;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; 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;
@ -97,29 +97,24 @@ public class CustomerController {
} }
@GetMapping @GetMapping
public ResponseEntity<List<CustomerResponse>> listCustomers( public ResponseEntity<PageResponse<CustomerResponse>> listCustomers(
@RequestParam(value = "type", required = false) CustomerType type, @RequestParam(defaultValue = "0") int page,
@RequestParam(value = "status", required = false) CustomerStatus status, @RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort,
Authentication authentication Authentication authentication
) { ) {
var actorId = extractActorId(authentication); var actorId = extractActorId(authentication);
logger.info("Listing customers by actor: {}", actorId.value()); logger.info("Listing customers by actor: {}", actorId.value());
Result<CustomerError, List<Customer>> result; var pageRequest = PageRequest.of(page, Math.min(size, 100),
if (type != null) { sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
result = listCustomers.executeByType(type); var result = listCustomers.execute(pageRequest);
} else if (status != null) {
result = listCustomers.executeByStatus(status);
} else {
result = listCustomers.execute();
}
if (result.isFailure()) { if (result.isFailure()) {
throw new CustomerDomainErrorException(result.unsafeGetError()); throw new CustomerDomainErrorException(result.unsafeGetError());
} }
var response = result.unsafeGetValue().stream().map(CustomerResponse::from).toList(); return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), CustomerResponse::from));
return ResponseEntity.ok(response);
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View file

@ -11,6 +11,9 @@ import de.effigenix.domain.masterdata.productcategory.ProductCategoryId;
import de.effigenix.infrastructure.masterdata.web.dto.CreateProductCategoryRequest; import de.effigenix.infrastructure.masterdata.web.dto.CreateProductCategoryRequest;
import de.effigenix.infrastructure.masterdata.web.dto.ProductCategoryResponse; import de.effigenix.infrastructure.masterdata.web.dto.ProductCategoryResponse;
import de.effigenix.infrastructure.masterdata.web.dto.UpdateProductCategoryRequest; import de.effigenix.infrastructure.masterdata.web.dto.UpdateProductCategoryRequest;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; 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;
@ -71,18 +74,24 @@ public class ProductCategoryController {
} }
@GetMapping @GetMapping
public ResponseEntity<List<ProductCategoryResponse>> listCategories(Authentication authentication) { public ResponseEntity<PageResponse<ProductCategoryResponse>> listCategories(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort,
Authentication authentication
) {
var actorId = extractActorId(authentication); var actorId = extractActorId(authentication);
logger.info("Listing product categories by actor: {}", actorId.value()); logger.info("Listing product categories by actor: {}", actorId.value());
var result = listProductCategories.execute(); var pageRequest = PageRequest.of(page, Math.min(size, 100),
sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
var result = listProductCategories.execute(pageRequest);
if (result.isFailure()) { if (result.isFailure()) {
throw new ProductCategoryDomainErrorException(result.unsafeGetError()); throw new ProductCategoryDomainErrorException(result.unsafeGetError());
} }
var response = result.unsafeGetValue().stream().map(ProductCategoryResponse::from).toList(); return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), ProductCategoryResponse::from));
return ResponseEntity.ok(response);
} }
@PutMapping("/{id}") @PutMapping("/{id}")

View file

@ -2,12 +2,14 @@ package de.effigenix.infrastructure.masterdata.web.controller;
import de.effigenix.application.masterdata.supplier.*; import de.effigenix.application.masterdata.supplier.*;
import de.effigenix.application.masterdata.supplier.command.*; import de.effigenix.application.masterdata.supplier.command.*;
import de.effigenix.domain.masterdata.supplier.Supplier;
import de.effigenix.domain.masterdata.supplier.SupplierError; import de.effigenix.domain.masterdata.supplier.SupplierError;
import de.effigenix.domain.masterdata.supplier.SupplierId; import de.effigenix.domain.masterdata.supplier.SupplierId;
import de.effigenix.domain.masterdata.supplier.SupplierStatus; import de.effigenix.domain.masterdata.supplier.SupplierStatus;
import de.effigenix.infrastructure.masterdata.web.dto.*; import de.effigenix.infrastructure.masterdata.web.dto.*;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; 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;
@ -88,26 +90,25 @@ public class SupplierController {
} }
@GetMapping @GetMapping
public ResponseEntity<List<SupplierResponse>> listSuppliers( public ResponseEntity<PageResponse<SupplierResponse>> listSuppliers(
@RequestParam(value = "status", required = false) SupplierStatus status, @RequestParam(value = "status", required = false) SupplierStatus status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort,
Authentication authentication Authentication authentication
) { ) {
var actorId = extractActorId(authentication); var actorId = extractActorId(authentication);
logger.info("Listing suppliers by actor: {}", actorId.value()); logger.info("Listing suppliers by actor: {}", actorId.value());
Result<SupplierError, List<Supplier>> result; var pageRequest = PageRequest.of(page, Math.min(size, 100),
if (status != null) { sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
result = listSuppliers.executeByStatus(status); var result = listSuppliers.execute(status, pageRequest);
} else {
result = listSuppliers.execute();
}
if (result.isFailure()) { if (result.isFailure()) {
throw new SupplierDomainErrorException(result.unsafeGetError()); throw new SupplierDomainErrorException(result.unsafeGetError());
} }
var response = result.unsafeGetValue().stream().map(SupplierResponse::from).toList(); return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), SupplierResponse::from));
return ResponseEntity.ok(response);
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View file

@ -2,10 +2,8 @@ package de.effigenix.infrastructure.production.persistence;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.domain.production.*; import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Quantity; import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -17,10 +15,7 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.ArrayList; import java.util.*;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
@Repository @Repository
@Profile("!no-db") @Profile("!no-db")
@ -28,6 +23,13 @@ public class JdbcBatchRepository implements BatchRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcBatchRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcBatchRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"batchNumber", "batch_number",
"status", "status",
"productionDate", "production_date",
"createdAt", "created_at"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcBatchRepository(JdbcClient jdbc) { public JdbcBatchRepository(JdbcClient jdbc) {
@ -138,6 +140,23 @@ public class JdbcBatchRepository implements BatchRepository {
} }
} }
@Override
public Result<RepositoryError, Page<Batch>> findAllSummary(PageRequest pageRequest) {
try {
long total = jdbc.sql("SELECT COUNT(*) FROM batches").query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY created_at DESC";
var batches = jdbc.sql("SELECT * FROM batches" + orderBy + " LIMIT :limit OFFSET :offset")
.param("limit", pageRequest.size())
.param("offset", pageRequest.offset())
.query(this::mapBatchRow).list();
return Result.success(Page.of(batches, pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAllSummary(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<Batch>> findAllSummary() { public Result<RepositoryError, List<Batch>> findAllSummary() {
try { try {

View file

@ -1,10 +1,8 @@
package de.effigenix.infrastructure.production.persistence; package de.effigenix.infrastructure.production.persistence;
import de.effigenix.domain.production.*; import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Quantity; import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -16,6 +14,7 @@ import java.sql.SQLException;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
@ -24,6 +23,13 @@ public class JdbcProductionOrderRepository implements ProductionOrderRepository
private static final Logger logger = LoggerFactory.getLogger(JdbcProductionOrderRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcProductionOrderRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"plannedDate", "planned_date",
"status", "status",
"priority", "priority",
"createdAt", "created_at"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcProductionOrderRepository(JdbcClient jdbc) { public JdbcProductionOrderRepository(JdbcClient jdbc) {
@ -44,6 +50,23 @@ public class JdbcProductionOrderRepository implements ProductionOrderRepository
} }
} }
@Override
public Result<RepositoryError, Page<ProductionOrder>> findAll(PageRequest pageRequest) {
try {
long total = jdbc.sql("SELECT COUNT(*) FROM production_orders").query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY planned_date, created_at DESC";
var orders = jdbc.sql("SELECT * FROM production_orders" + orderBy + " LIMIT :limit OFFSET :offset")
.param("limit", pageRequest.size())
.param("offset", pageRequest.offset())
.query(this::mapRow).list();
return Result.success(Page.of(orders, pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<ProductionOrder>> findAll() { public Result<RepositoryError, List<ProductionOrder>> findAll() {
try { try {

View file

@ -1,10 +1,8 @@
package de.effigenix.infrastructure.production.persistence; package de.effigenix.infrastructure.production.persistence;
import de.effigenix.domain.production.*; import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Quantity; import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.*;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -15,6 +13,7 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
@Repository @Repository
@ -23,6 +22,13 @@ public class JdbcRecipeRepository implements RecipeRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcRecipeRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcRecipeRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"name", "name",
"version", "version",
"status", "status",
"createdAt", "created_at"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcRecipeRepository(JdbcClient jdbc) { public JdbcRecipeRepository(JdbcClient jdbc) {
@ -47,6 +53,24 @@ public class JdbcRecipeRepository implements RecipeRepository {
} }
} }
@Override
public Result<RepositoryError, Page<Recipe>> findAll(PageRequest pageRequest) {
try {
long total = jdbc.sql("SELECT COUNT(*) FROM recipes").query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY name, version";
var recipes = jdbc.sql("SELECT * FROM recipes" + orderBy + " LIMIT :limit OFFSET :offset")
.param("limit", pageRequest.size())
.param("offset", pageRequest.offset())
.query(this::mapRecipeRow).list()
.stream().map(this::loadChildren).toList();
return Result.success(Page.of(recipes, pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<Recipe>> findAll() { public Result<RepositoryError, List<Recipe>> findAll() {
try { try {

View file

@ -30,6 +30,10 @@ import de.effigenix.infrastructure.production.web.dto.CancelBatchRequest;
import de.effigenix.infrastructure.production.web.dto.CompleteBatchRequest; import de.effigenix.infrastructure.production.web.dto.CompleteBatchRequest;
import de.effigenix.infrastructure.production.web.dto.PlanBatchRequest; import de.effigenix.infrastructure.production.web.dto.PlanBatchRequest;
import de.effigenix.infrastructure.production.web.dto.RecordConsumptionRequest; import de.effigenix.infrastructure.production.web.dto.RecordConsumptionRequest;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; 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;
@ -100,17 +104,34 @@ public class BatchController {
@GetMapping @GetMapping
@PreAuthorize("hasAuthority('BATCH_READ')") @PreAuthorize("hasAuthority('BATCH_READ')")
public ResponseEntity<List<BatchSummaryResponse>> listBatches( public ResponseEntity<PageResponse<BatchSummaryResponse>> listBatches(
@RequestParam(value = "status", required = false) String status, @RequestParam(value = "status", required = false) String status,
@RequestParam(value = "productionDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate productionDate, @RequestParam(value = "productionDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate productionDate,
@RequestParam(value = "articleId", required = false) String articleId, @RequestParam(value = "articleId", required = false) String articleId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort,
Authentication authentication Authentication authentication
) { ) {
var actorId = ActorId.of(authentication.getName()); var actorId = ActorId.of(authentication.getName());
var pageRequest = PageRequest.of(page, Math.min(size, 100),
sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
var result = switch (filterType(status, productionDate, articleId)) { var filterKey = filterType(status, productionDate, articleId);
case "ambiguous" -> throw new BatchDomainErrorException( if ("ambiguous".equals(filterKey)) {
throw new BatchDomainErrorException(
new BatchError.ValidationFailure("Only one filter allowed at a time: status, productionDate, or articleId")); new BatchError.ValidationFailure("Only one filter allowed at a time: status, productionDate, or articleId"));
}
if ("none".equals(filterKey)) {
var result = listBatches.execute(actorId, pageRequest);
if (result.isFailure()) {
throw new BatchDomainErrorException(result.unsafeGetError());
}
return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), BatchSummaryResponse::from));
}
var result = switch (filterKey) {
case "status" -> { case "status" -> {
try { try {
yield listBatches.executeByStatus(BatchStatus.valueOf(status), actorId); yield listBatches.executeByStatus(BatchStatus.valueOf(status), actorId);
@ -121,17 +142,16 @@ public class BatchController {
} }
case "productionDate" -> listBatches.executeByProductionDate(productionDate, actorId); case "productionDate" -> listBatches.executeByProductionDate(productionDate, actorId);
case "articleId" -> listBatches.executeByArticleId(articleId, actorId); case "articleId" -> listBatches.executeByArticleId(articleId, actorId);
default -> listBatches.execute(actorId); default -> throw new IllegalStateException("Unexpected filter: " + filterKey);
}; };
if (result.isFailure()) { if (result.isFailure()) {
throw new BatchDomainErrorException(result.unsafeGetError()); throw new BatchDomainErrorException(result.unsafeGetError());
} }
var summaries = result.unsafeGetValue().stream() var list = result.unsafeGetValue();
.map(BatchSummaryResponse::from) var batchPage = Page.of(list, 0, list.size() > 0 ? list.size() : 20, list.size());
.toList(); return ResponseEntity.ok(PageResponse.from(batchPage, BatchSummaryResponse::from));
return ResponseEntity.ok(summaries);
} }
@GetMapping("/by-number/{batchNumber}") @GetMapping("/by-number/{batchNumber}")

View file

@ -22,7 +22,10 @@ import de.effigenix.infrastructure.production.web.dto.CancelProductionOrderReque
import de.effigenix.infrastructure.production.web.dto.CreateProductionOrderRequest; import de.effigenix.infrastructure.production.web.dto.CreateProductionOrderRequest;
import de.effigenix.infrastructure.production.web.dto.ProductionOrderResponse; import de.effigenix.infrastructure.production.web.dto.ProductionOrderResponse;
import de.effigenix.infrastructure.production.web.dto.RescheduleProductionOrderRequest; import de.effigenix.infrastructure.production.web.dto.RescheduleProductionOrderRequest;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; 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;
@ -79,15 +82,20 @@ public class ProductionOrderController {
@GetMapping @GetMapping
@PreAuthorize("hasAuthority('PRODUCTION_ORDER_READ')") @PreAuthorize("hasAuthority('PRODUCTION_ORDER_READ')")
public ResponseEntity<List<ProductionOrderResponse>> listProductionOrders( public ResponseEntity<PageResponse<ProductionOrderResponse>> listProductionOrders(
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate dateFrom, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate dateFrom,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate dateTo, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate dateTo,
@RequestParam(required = false) ProductionOrderStatus status, @RequestParam(required = false) ProductionOrderStatus status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort,
Authentication authentication Authentication authentication
) { ) {
logger.info("Listing production orders by actor: {}", authentication.getName()); logger.info("Listing production orders by actor: {}", authentication.getName());
var actor = ActorId.of(authentication.getName()); var actor = ActorId.of(authentication.getName());
var pageRequest = PageRequest.of(page, Math.min(size, 100),
sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
boolean hasDateRange = dateFrom != null && dateTo != null; boolean hasDateRange = dateFrom != null && dateTo != null;
boolean hasPartialDate = (dateFrom != null) != (dateTo != null); boolean hasPartialDate = (dateFrom != null) != (dateTo != null);
@ -96,22 +104,31 @@ public class ProductionOrderController {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
boolean hasFilter = hasDateRange || status != null;
if (!hasFilter) {
var result = listProductionOrders.execute(actor, pageRequest);
if (result.isFailure()) {
throw new ProductionOrderDomainErrorException(result.unsafeGetError());
}
return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(),
order -> ProductionOrderResponse.from(order, resolveBatchNumber(order))));
}
var result = hasDateRange && status != null var result = hasDateRange && status != null
? listProductionOrders.executeByDateRangeAndStatus(dateFrom, dateTo, status, actor) ? listProductionOrders.executeByDateRangeAndStatus(dateFrom, dateTo, status, actor)
: hasDateRange : hasDateRange
? listProductionOrders.executeByDateRange(dateFrom, dateTo, actor) ? listProductionOrders.executeByDateRange(dateFrom, dateTo, actor)
: status != null : listProductionOrders.executeByStatus(status, actor);
? listProductionOrders.executeByStatus(status, actor)
: listProductionOrders.execute(actor);
if (result.isFailure()) { if (result.isFailure()) {
throw new ProductionOrderDomainErrorException(result.unsafeGetError()); throw new ProductionOrderDomainErrorException(result.unsafeGetError());
} }
var responses = result.unsafeGetValue().stream() var list = result.unsafeGetValue();
.map(order -> ProductionOrderResponse.from(order, resolveBatchNumber(order))) var orderPage = Page.of(list, 0, list.size() > 0 ? list.size() : 20, list.size());
.toList(); return ResponseEntity.ok(PageResponse.from(orderPage,
return ResponseEntity.ok(responses); order -> ProductionOrderResponse.from(order, resolveBatchNumber(order))));
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View file

@ -24,6 +24,10 @@ import de.effigenix.infrastructure.production.web.dto.AddRecipeIngredientRequest
import de.effigenix.infrastructure.production.web.dto.CreateRecipeRequest; import de.effigenix.infrastructure.production.web.dto.CreateRecipeRequest;
import de.effigenix.infrastructure.production.web.dto.RecipeResponse; import de.effigenix.infrastructure.production.web.dto.RecipeResponse;
import de.effigenix.infrastructure.production.web.dto.RecipeSummaryResponse; import de.effigenix.infrastructure.production.web.dto.RecipeSummaryResponse;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; 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;
@ -94,11 +98,16 @@ public class RecipeController {
@GetMapping @GetMapping
@PreAuthorize("hasAuthority('RECIPE_READ')") @PreAuthorize("hasAuthority('RECIPE_READ')")
public ResponseEntity<List<RecipeSummaryResponse>> listRecipes( public ResponseEntity<PageResponse<RecipeSummaryResponse>> listRecipes(
@RequestParam(value = "status", required = false) String status, @RequestParam(value = "status", required = false) String status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort,
Authentication authentication Authentication authentication
) { ) {
var actorId = extractActorId(authentication); var actorId = extractActorId(authentication);
var pageRequest = PageRequest.of(page, Math.min(size, 100),
sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
RecipeStatus parsedStatus = null; RecipeStatus parsedStatus = null;
if (status != null) { if (status != null) {
@ -110,18 +119,22 @@ public class RecipeController {
} }
} }
var result = (parsedStatus != null) if (parsedStatus != null) {
? listRecipes.executeByStatus(parsedStatus, actorId) var result = listRecipes.executeByStatus(parsedStatus, actorId);
: listRecipes.execute(actorId); if (result.isFailure()) {
throw new RecipeDomainErrorException(result.unsafeGetError());
}
var list = result.unsafeGetValue();
var recipePage = Page.of(list, 0, list.size() > 0 ? list.size() : 20, list.size());
return ResponseEntity.ok(PageResponse.from(recipePage, RecipeSummaryResponse::from));
}
var result = listRecipes.execute(actorId, pageRequest);
if (result.isFailure()) { if (result.isFailure()) {
throw new RecipeDomainErrorException(result.unsafeGetError()); throw new RecipeDomainErrorException(result.unsafeGetError());
} }
var summaries = result.unsafeGetValue().stream() return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), RecipeSummaryResponse::from));
.map(RecipeSummaryResponse::from)
.toList();
return ResponseEntity.ok(summaries);
} }
@PostMapping @PostMapping

View file

@ -2,6 +2,8 @@ package de.effigenix.infrastructure.shared;
import de.effigenix.shared.common.Country; import de.effigenix.shared.common.Country;
import de.effigenix.shared.common.CountryRepository; import de.effigenix.shared.common.CountryRepository;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -286,6 +288,15 @@ public class InMemoryCountryRepository implements CountryRepository {
return allSorted; return allSorted;
} }
@Override
public Page<Country> findAll(PageRequest pageRequest) {
int total = allSorted.size();
int fromIndex = Math.min(pageRequest.offset(), total);
int toIndex = Math.min(fromIndex + pageRequest.size(), total);
var content = allSorted.subList(fromIndex, toIndex);
return Page.of(content, pageRequest.page(), pageRequest.size(), total);
}
@Override @Override
public List<Country> search(String query) { public List<Country> search(String query) {
if (query == null || query.isBlank()) { if (query == null || query.isBlank()) {

View file

@ -0,0 +1,23 @@
package de.effigenix.infrastructure.shared.persistence;
import de.effigenix.shared.common.SortField;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public final class PaginationHelper {
private PaginationHelper() {}
public static String buildOrderByClause(List<SortField> sort, Map<String, String> allowedFields) {
if (sort == null || sort.isEmpty()) {
return "";
}
var clauses = sort.stream()
.filter(sf -> allowedFields.containsKey(sf.field()))
.map(sf -> allowedFields.get(sf.field()) + " " + sf.direction().name())
.collect(Collectors.joining(", "));
return clauses.isEmpty() ? "" : " ORDER BY " + clauses;
}
}

View file

@ -1,6 +1,9 @@
package de.effigenix.infrastructure.shared.web; package de.effigenix.infrastructure.shared.web;
import de.effigenix.application.shared.ListCountries; import de.effigenix.application.shared.ListCountries;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.SortField;
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 org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -26,9 +29,21 @@ public class CountryController {
public record CountryResponse(String code, String name) {} public record CountryResponse(String code, String name) {}
@GetMapping @GetMapping
public ResponseEntity<PageResponse<CountryResponse>> list(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort) {
var pageRequest = PageRequest.of(page, Math.min(size, 100),
sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
var countryPage = listCountries.execute(pageRequest);
return ResponseEntity.ok(PageResponse.from(countryPage,
c -> new CountryResponse(c.code(), c.name())));
}
@GetMapping(params = "q")
public ResponseEntity<List<CountryResponse>> search( public ResponseEntity<List<CountryResponse>> search(
@RequestParam(name = "q", required = false, defaultValue = "") String query) { @RequestParam(name = "q") String query) {
var countries = listCountries.execute(query); var countries = listCountries.search(query);
var response = countries.stream() var response = countries.stream()
.map(c -> new CountryResponse(c.code(), c.name())) .map(c -> new CountryResponse(c.code(), c.name()))
.toList(); .toList();

View file

@ -0,0 +1,26 @@
package de.effigenix.infrastructure.shared.web.dto;
import de.effigenix.shared.common.Page;
import java.util.List;
import java.util.function.Function;
public record PageResponse<T>(
List<T> content,
PageInfo page
) {
public record PageInfo(int number, int size, long totalElements, int totalPages) {}
public static <T> PageResponse<T> from(Page<T> domainPage) {
return new PageResponse<>(domainPage.content(),
new PageInfo(domainPage.number(), domainPage.size(),
domainPage.totalElements(), domainPage.totalPages()));
}
public static <D, R> PageResponse<R> from(Page<D> domainPage, Function<D, R> mapper) {
var content = domainPage.content().stream().map(mapper).toList();
return new PageResponse<>(content,
new PageInfo(domainPage.number(), domainPage.size(),
domainPage.totalElements(), domainPage.totalPages()));
}
}

View file

@ -6,6 +6,8 @@ import de.effigenix.domain.masterdata.article.ArticleNumber;
import de.effigenix.domain.masterdata.article.ArticleRepository; import de.effigenix.domain.masterdata.article.ArticleRepository;
import de.effigenix.domain.masterdata.article.ArticleStatus; import de.effigenix.domain.masterdata.article.ArticleStatus;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -26,6 +28,11 @@ public class StubArticleRepository implements ArticleRepository {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<Article>> findAll(ArticleStatus status, PageRequest pageRequest) {
return Result.success(Page.empty(pageRequest.size()));
}
@Override @Override
public Result<RepositoryError, List<Article>> findAll() { public Result<RepositoryError, List<Article>> findAll() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -7,6 +7,8 @@ import de.effigenix.domain.production.BatchRepository;
import de.effigenix.domain.production.BatchStatus; import de.effigenix.domain.production.BatchStatus;
import de.effigenix.domain.production.RecipeId; import de.effigenix.domain.production.RecipeId;
import de.effigenix.domain.production.TracedBatch; import de.effigenix.domain.production.TracedBatch;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -53,6 +55,11 @@ public class StubBatchRepository implements BatchRepository {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<Batch>> findAllSummary(PageRequest pageRequest) {
return Result.success(Page.empty(pageRequest.size()));
}
@Override @Override
public Result<RepositoryError, List<Batch>> findAllSummary() { public Result<RepositoryError, List<Batch>> findAllSummary() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -6,6 +6,8 @@ import de.effigenix.domain.masterdata.customer.CustomerName;
import de.effigenix.domain.masterdata.customer.CustomerRepository; import de.effigenix.domain.masterdata.customer.CustomerRepository;
import de.effigenix.domain.masterdata.customer.CustomerStatus; import de.effigenix.domain.masterdata.customer.CustomerStatus;
import de.effigenix.domain.masterdata.customer.CustomerType; import de.effigenix.domain.masterdata.customer.CustomerType;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -26,6 +28,11 @@ public class StubCustomerRepository implements CustomerRepository {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<Customer>> findAll(PageRequest pageRequest) {
return Result.success(Page.empty(pageRequest.size()));
}
@Override @Override
public Result<RepositoryError, List<Customer>> findAll() { public Result<RepositoryError, List<Customer>> findAll() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -5,6 +5,8 @@ import de.effigenix.domain.inventory.InventoryCountId;
import de.effigenix.domain.inventory.InventoryCountRepository; import de.effigenix.domain.inventory.InventoryCountRepository;
import de.effigenix.domain.inventory.InventoryCountStatus; import de.effigenix.domain.inventory.InventoryCountStatus;
import de.effigenix.domain.inventory.StorageLocationId; import de.effigenix.domain.inventory.StorageLocationId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -25,6 +27,11 @@ public class StubInventoryCountRepository implements InventoryCountRepository {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<InventoryCount>> findAll(InventoryCountStatus status, PageRequest pageRequest) {
return Result.failure(STUB_ERROR);
}
@Override @Override
public Result<RepositoryError, List<InventoryCount>> findAll() { public Result<RepositoryError, List<InventoryCount>> findAll() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -4,6 +4,8 @@ import de.effigenix.domain.masterdata.productcategory.CategoryName;
import de.effigenix.domain.masterdata.productcategory.ProductCategory; import de.effigenix.domain.masterdata.productcategory.ProductCategory;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryRepository; import de.effigenix.domain.masterdata.productcategory.ProductCategoryRepository;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -24,6 +26,11 @@ public class StubProductCategoryRepository implements ProductCategoryRepository
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<ProductCategory>> findAll(PageRequest pageRequest) {
return Result.success(Page.empty(pageRequest.size()));
}
@Override @Override
public Result<RepositoryError, List<ProductCategory>> findAll() { public Result<RepositoryError, List<ProductCategory>> findAll() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -4,6 +4,8 @@ import de.effigenix.domain.production.ProductionOrder;
import de.effigenix.domain.production.ProductionOrderId; import de.effigenix.domain.production.ProductionOrderId;
import de.effigenix.domain.production.ProductionOrderRepository; import de.effigenix.domain.production.ProductionOrderRepository;
import de.effigenix.domain.production.ProductionOrderStatus; import de.effigenix.domain.production.ProductionOrderStatus;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -25,6 +27,11 @@ public class StubProductionOrderRepository implements ProductionOrderRepository
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<ProductionOrder>> findAll(PageRequest pageRequest) {
return Result.success(Page.empty(pageRequest.size()));
}
@Override @Override
public Result<RepositoryError, List<ProductionOrder>> findAll() { public Result<RepositoryError, List<ProductionOrder>> findAll() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -4,6 +4,8 @@ import de.effigenix.domain.production.Recipe;
import de.effigenix.domain.production.RecipeId; import de.effigenix.domain.production.RecipeId;
import de.effigenix.domain.production.RecipeRepository; import de.effigenix.domain.production.RecipeRepository;
import de.effigenix.domain.production.RecipeStatus; import de.effigenix.domain.production.RecipeStatus;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -24,6 +26,11 @@ public class StubRecipeRepository implements RecipeRepository {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<Recipe>> findAll(PageRequest pageRequest) {
return Result.success(Page.empty(pageRequest.size()));
}
@Override @Override
public Result<RepositoryError, List<Recipe>> findAll() { public Result<RepositoryError, List<Recipe>> findAll() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -4,6 +4,8 @@ import de.effigenix.domain.usermanagement.Role;
import de.effigenix.domain.usermanagement.RoleId; import de.effigenix.domain.usermanagement.RoleId;
import de.effigenix.domain.usermanagement.RoleName; import de.effigenix.domain.usermanagement.RoleName;
import de.effigenix.domain.usermanagement.RoleRepository; import de.effigenix.domain.usermanagement.RoleRepository;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -29,6 +31,11 @@ public class StubRoleRepository implements RoleRepository {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<Role>> findAll(PageRequest pageRequest) {
return Result.success(Page.empty(pageRequest.size()));
}
@Override @Override
public Result<RepositoryError, List<Role>> findAll() { public Result<RepositoryError, List<Role>> findAll() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -6,6 +6,8 @@ import de.effigenix.domain.inventory.StockMovement;
import de.effigenix.domain.inventory.StockMovementId; import de.effigenix.domain.inventory.StockMovementId;
import de.effigenix.domain.inventory.StockMovementRepository; import de.effigenix.domain.inventory.StockMovementRepository;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -27,6 +29,14 @@ public class StubStockMovementRepository implements StockMovementRepository {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<StockMovement>> findAll(StockId stockId, ArticleId articleId,
MovementType movementType, String batchRef,
Instant from, Instant to,
PageRequest pageRequest) {
return Result.failure(STUB_ERROR);
}
@Override @Override
public Result<RepositoryError, List<StockMovement>> findAll() { public Result<RepositoryError, List<StockMovement>> findAll() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -5,6 +5,8 @@ import de.effigenix.domain.inventory.StockId;
import de.effigenix.domain.inventory.StockRepository; import de.effigenix.domain.inventory.StockRepository;
import de.effigenix.domain.inventory.StorageLocationId; import de.effigenix.domain.inventory.StorageLocationId;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -26,6 +28,21 @@ public class StubStockRepository implements StockRepository {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<Stock>> findAll(PageRequest pageRequest) {
return Result.failure(STUB_ERROR);
}
@Override
public Result<RepositoryError, Page<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId, PageRequest pageRequest) {
return Result.failure(STUB_ERROR);
}
@Override
public Result<RepositoryError, Page<Stock>> findAllByArticleId(ArticleId articleId, PageRequest pageRequest) {
return Result.failure(STUB_ERROR);
}
@Override @Override
public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
@ -61,6 +78,11 @@ public class StubStockRepository implements StockRepository {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, List<Stock>> findAllByBatchId(String batchId) {
return Result.failure(STUB_ERROR);
}
@Override @Override
public Result<RepositoryError, Void> save(Stock stock) { public Result<RepositoryError, Void> save(Stock stock) {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -5,6 +5,8 @@ import de.effigenix.domain.inventory.StorageLocationId;
import de.effigenix.domain.inventory.StorageLocationName; import de.effigenix.domain.inventory.StorageLocationName;
import de.effigenix.domain.inventory.StorageLocationRepository; import de.effigenix.domain.inventory.StorageLocationRepository;
import de.effigenix.domain.inventory.StorageType; import de.effigenix.domain.inventory.StorageType;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -25,6 +27,11 @@ public class StubStorageLocationRepository implements StorageLocationRepository
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<StorageLocation>> findAll(StorageType storageType, Boolean active, PageRequest pageRequest) {
return Result.failure(STUB_ERROR);
}
@Override @Override
public Result<RepositoryError, List<StorageLocation>> findAll() { public Result<RepositoryError, List<StorageLocation>> findAll() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -5,6 +5,8 @@ import de.effigenix.domain.masterdata.supplier.SupplierId;
import de.effigenix.domain.masterdata.supplier.SupplierName; import de.effigenix.domain.masterdata.supplier.SupplierName;
import de.effigenix.domain.masterdata.supplier.SupplierRepository; import de.effigenix.domain.masterdata.supplier.SupplierRepository;
import de.effigenix.domain.masterdata.supplier.SupplierStatus; import de.effigenix.domain.masterdata.supplier.SupplierStatus;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -25,6 +27,11 @@ public class StubSupplierRepository implements SupplierRepository {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<Supplier>> findAll(SupplierStatus status, PageRequest pageRequest) {
return Result.success(Page.empty(pageRequest.size()));
}
@Override @Override
public Result<RepositoryError, List<Supplier>> findAll() { public Result<RepositoryError, List<Supplier>> findAll() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -4,6 +4,8 @@ import de.effigenix.domain.usermanagement.User;
import de.effigenix.domain.usermanagement.UserId; import de.effigenix.domain.usermanagement.UserId;
import de.effigenix.domain.usermanagement.UserRepository; import de.effigenix.domain.usermanagement.UserRepository;
import de.effigenix.domain.usermanagement.UserStatus; import de.effigenix.domain.usermanagement.UserStatus;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -44,6 +46,11 @@ public class StubUserRepository implements UserRepository {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);
} }
@Override
public Result<RepositoryError, Page<User>> findAll(String branchId, PageRequest pageRequest) {
return Result.success(Page.empty(pageRequest.size()));
}
@Override @Override
public Result<RepositoryError, List<User>> findAll() { public Result<RepositoryError, List<User>> findAll() {
return Result.failure(STUB_ERROR); return Result.failure(STUB_ERROR);

View file

@ -1,8 +1,8 @@
package de.effigenix.infrastructure.usermanagement.persistence; package de.effigenix.infrastructure.usermanagement.persistence;
import de.effigenix.domain.usermanagement.*; import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -19,6 +19,10 @@ public class JdbcRoleRepository implements RoleRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcRoleRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcRoleRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"name", "name"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
public JdbcRoleRepository(JdbcClient jdbc) { public JdbcRoleRepository(JdbcClient jdbc) {
@ -59,6 +63,24 @@ public class JdbcRoleRepository implements RoleRepository {
} }
} }
@Override
public Result<RepositoryError, Page<Role>> findAll(PageRequest pageRequest) {
try {
long total = jdbc.sql("SELECT COUNT(*) FROM roles").query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY name";
var roles = jdbc.sql("SELECT * FROM roles" + orderBy + " LIMIT :limit OFFSET :offset")
.param("limit", pageRequest.size())
.param("offset", pageRequest.offset())
.query(this::mapRoleRow).list()
.stream().map(this::loadPermissions).toList();
return Result.success(Page.of(roles, pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<Role>> findAll() { public Result<RepositoryError, List<Role>> findAll() {
try { try {

View file

@ -1,8 +1,8 @@
package de.effigenix.infrastructure.usermanagement.persistence; package de.effigenix.infrastructure.usermanagement.persistence;
import de.effigenix.domain.usermanagement.*; import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.infrastructure.shared.persistence.PaginationHelper;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
@ -20,6 +20,13 @@ public class JdbcUserRepository implements UserRepository {
private static final Logger logger = LoggerFactory.getLogger(JdbcUserRepository.class); private static final Logger logger = LoggerFactory.getLogger(JdbcUserRepository.class);
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"username", "username",
"email", "email",
"status", "status",
"createdAt", "created_at"
);
private final JdbcClient jdbc; private final JdbcClient jdbc;
private final RoleRepository roleRepository; private final RoleRepository roleRepository;
@ -113,6 +120,31 @@ public class JdbcUserRepository implements UserRepository {
} }
} }
@Override
public Result<RepositoryError, Page<User>> findAll(String branchId, PageRequest pageRequest) {
try {
String where = "";
var params = new LinkedHashMap<String, Object>();
if (branchId != null) {
where = " WHERE branch_id = :branchId";
params.put("branchId", branchId);
}
long total = jdbc.sql("SELECT COUNT(*) FROM users" + where)
.params(params).query(Long.class).single();
String orderBy = PaginationHelper.buildOrderByClause(pageRequest.sort(), SORT_FIELD_MAP);
if (orderBy.isEmpty()) orderBy = " ORDER BY username";
params.put("limit", pageRequest.size());
params.put("offset", pageRequest.offset());
var users = jdbc.sql("SELECT * FROM users" + where + orderBy + " LIMIT :limit OFFSET :offset")
.params(params).query(this::mapUserRow).list()
.stream().map(this::loadRoles).toList();
return Result.success(Page.of(users, pageRequest.page(), pageRequest.size(), total));
} catch (Exception e) {
logger.trace("Database error in findAll(paginated)", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override @Override
public Result<RepositoryError, List<User>> findAll() { public Result<RepositoryError, List<User>> findAll() {
try { try {

View file

@ -1,9 +1,11 @@
package de.effigenix.infrastructure.usermanagement.web.controller; package de.effigenix.infrastructure.usermanagement.web.controller;
import de.effigenix.application.usermanagement.dto.RoleDTO; import de.effigenix.application.usermanagement.dto.RoleDTO;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.domain.usermanagement.Role; import de.effigenix.shared.common.SortField;
import de.effigenix.domain.usermanagement.RoleRepository; import de.effigenix.domain.usermanagement.RoleRepository;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -20,10 +22,10 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* REST Controller for Role Management endpoints. * REST Controller for Role Management endpoints.
@ -99,20 +101,24 @@ public class RoleController {
@ApiResponse(responseCode = "403", description = "Missing USER_MANAGEMENT permission"), @ApiResponse(responseCode = "403", description = "Missing USER_MANAGEMENT permission"),
@ApiResponse(responseCode = "401", description = "Authentication required") @ApiResponse(responseCode = "401", description = "Authentication required")
}) })
public ResponseEntity<List<RoleDTO>> listRoles(Authentication authentication) { public ResponseEntity<PageResponse<RoleDTO>> listRoles(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort,
Authentication authentication
) {
ActorId actorId = extractActorId(authentication); ActorId actorId = extractActorId(authentication);
logger.info("Listing roles by actor: {}", actorId.value()); logger.info("Listing roles by actor: {}", actorId.value());
var result = roleRepository.findAll(); var pageRequest = PageRequest.of(page, Math.min(size, 100),
sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
var result = roleRepository.findAll(pageRequest);
if (result.isFailure()) { if (result.isFailure()) {
throw new RoleDomainErrorException(result.unsafeGetError()); throw new RoleDomainErrorException(result.unsafeGetError());
} }
List<RoleDTO> roles = result.unsafeGetValue().stream() logger.info("Found {} roles", result.unsafeGetValue().totalElements());
.map(RoleDTO::from) return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue(), RoleDTO::from));
.collect(Collectors.toList());
logger.info("Found {} roles", roles.size());
return ResponseEntity.ok(roles);
} }
// ==================== Helper Methods ==================== // ==================== Helper Methods ====================

View file

@ -5,8 +5,11 @@ import de.effigenix.application.usermanagement.command.*;
import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.application.usermanagement.dto.UserDTO;
import de.effigenix.domain.usermanagement.RoleName; import de.effigenix.domain.usermanagement.RoleName;
import de.effigenix.domain.usermanagement.UserError; import de.effigenix.domain.usermanagement.UserError;
import de.effigenix.infrastructure.shared.web.dto.PageResponse;
import de.effigenix.infrastructure.usermanagement.web.dto.*; import de.effigenix.infrastructure.usermanagement.web.dto.*;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.SortField;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@ -111,14 +114,22 @@ public class UserController {
@ApiResponse(responseCode = "200", description = "Users retrieved successfully"), @ApiResponse(responseCode = "200", description = "Users retrieved successfully"),
@ApiResponse(responseCode = "401", description = "Authentication required") @ApiResponse(responseCode = "401", description = "Authentication required")
}) })
public ResponseEntity<List<UserDTO>> listUsers(Authentication authentication) { public ResponseEntity<PageResponse<UserDTO>> listUsers(
@RequestParam(value = "branchId", required = false) String branchId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) List<String> sort,
Authentication authentication
) {
ActorId actorId = extractActorId(authentication); ActorId actorId = extractActorId(authentication);
logger.info("Listing users by actor: {}", actorId.value()); logger.info("Listing users by actor: {}", actorId.value());
Result<UserError, List<UserDTO>> result = listUsers.execute(actorId); var pageRequest = PageRequest.of(page, Math.min(size, 100),
sort != null ? sort.stream().map(SortField::parse).toList() : List.of());
Result<UserError, de.effigenix.shared.common.Page<UserDTO>> result = listUsers.execute(branchId, actorId, pageRequest);
if (result.isFailure()) throw new DomainErrorException(result.unsafeGetError()); if (result.isFailure()) throw new DomainErrorException(result.unsafeGetError());
return ResponseEntity.ok(result.unsafeGetValue()); return ResponseEntity.ok(PageResponse.from(result.unsafeGetValue()));
} }
@GetMapping("/{id}") @GetMapping("/{id}")

View file

@ -5,6 +5,7 @@ import java.util.Optional;
public interface CountryRepository { public interface CountryRepository {
List<Country> findAll(); List<Country> findAll();
Page<Country> findAll(PageRequest pageRequest);
List<Country> search(String query); List<Country> search(String query);
Optional<Country> findByCode(String code); Optional<Country> findByCode(String code);
} }

View file

@ -0,0 +1,25 @@
package de.effigenix.shared.common;
import java.util.List;
import java.util.function.Function;
public record Page<T>(List<T> content, int number, int size, long totalElements, int totalPages) {
public Page {
content = content != null ? List.copyOf(content) : List.of();
}
public static <T> Page<T> of(List<T> content, int number, int size, long totalElements) {
int totalPages = size > 0 ? (int) Math.ceil((double) totalElements / size) : 0;
return new Page<>(content, number, size, totalElements, totalPages);
}
public static <T> Page<T> empty(int size) {
return new Page<>(List.of(), 0, size, 0, 0);
}
public <U> Page<U> map(Function<T, U> mapper) {
var mapped = content.stream().map(mapper).toList();
return new Page<>(mapped, number, size, totalElements, totalPages);
}
}

View file

@ -0,0 +1,30 @@
package de.effigenix.shared.common;
import java.util.List;
public record PageRequest(int page, int size, List<SortField> sort) {
private static final int MAX_SIZE = 100;
public PageRequest {
if (page < 0) {
throw new IllegalArgumentException("Page must be >= 0, got: " + page);
}
if (size < 1 || size > MAX_SIZE) {
throw new IllegalArgumentException("Size must be between 1 and " + MAX_SIZE + ", got: " + size);
}
sort = sort != null ? List.copyOf(sort) : List.of();
}
public static PageRequest of(int page, int size) {
return new PageRequest(page, size, List.of());
}
public static PageRequest of(int page, int size, List<SortField> sort) {
return new PageRequest(page, size, sort);
}
public int offset() {
return page * size;
}
}

View file

@ -0,0 +1,5 @@
package de.effigenix.shared.common;
public enum SortDirection {
ASC, DESC
}

View file

@ -0,0 +1,31 @@
package de.effigenix.shared.common;
public record SortField(String field, SortDirection direction) {
public static SortField asc(String field) {
return new SortField(field, SortDirection.ASC);
}
public static SortField desc(String field) {
return new SortField(field, SortDirection.DESC);
}
public static SortField parse(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Sort field must not be blank");
}
var parts = value.split(",", 2);
var field = parts[0].trim();
if (field.isEmpty()) {
throw new IllegalArgumentException("Sort field name must not be empty");
}
var direction = SortDirection.ASC;
if (parts.length == 2) {
var dirStr = parts[1].trim().toUpperCase();
if ("DESC".equals(dirStr)) {
direction = SortDirection.DESC;
}
}
return new SortField(field, direction);
}
}

View file

@ -2,6 +2,8 @@ package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -261,10 +263,14 @@ class CheckStockExpiryTest {
// Unused methods for this test // Unused methods for this test
@Override public Result<RepositoryError, List<Stock>> findAllBelowMinimumLevel() { return Result.success(List.of()); } @Override public Result<RepositoryError, List<Stock>> findAllBelowMinimumLevel() { return Result.success(List.of()); }
@Override public Result<RepositoryError, Optional<Stock>> findById(StockId id) { return Result.success(Optional.empty()); } @Override public Result<RepositoryError, Optional<Stock>> findById(StockId id) { return Result.success(Optional.empty()); }
@Override public Result<RepositoryError, Page<Stock>> findAll(PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); }
@Override public Result<RepositoryError, Page<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId, PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); }
@Override public Result<RepositoryError, Page<Stock>> findAllByArticleId(ArticleId articleId, PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); }
@Override public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(Optional.empty()); } @Override public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(Optional.empty()); }
@Override public Result<RepositoryError, Boolean> existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(false); } @Override public Result<RepositoryError, Boolean> existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(false); }
@Override public Result<RepositoryError, List<Stock>> findAll() { return Result.success(List.of()); } @Override public Result<RepositoryError, List<Stock>> findAll() { return Result.success(List.of()); }
@Override public Result<RepositoryError, List<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId) { return Result.success(List.of()); } @Override public Result<RepositoryError, List<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId) { return Result.success(List.of()); }
@Override public Result<RepositoryError, List<Stock>> findAllByArticleId(ArticleId articleId) { return Result.success(List.of()); } @Override public Result<RepositoryError, List<Stock>> findAllByArticleId(ArticleId articleId) { return Result.success(List.of()); }
@Override public Result<RepositoryError, List<Stock>> findAllByBatchId(String batchId) { return Result.success(List.of()); }
} }
} }

View file

@ -1,6 +1,8 @@
package de.effigenix.application.inventory; package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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.ActorId;
@ -18,6 +20,8 @@ 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.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -31,6 +35,7 @@ class ListInventoryCountsTest {
private InventoryCount count1; private InventoryCount count1;
private InventoryCount count2; private InventoryCount count2;
private final ActorId actorId = ActorId.of("user-1"); private final ActorId actorId = ActorId.of("user-1");
private final PageRequest pageRequest = PageRequest.of(0, 100);
@BeforeEach @BeforeEach
void setUp() { void setUp() {
@ -65,105 +70,55 @@ class ListInventoryCountsTest {
@Test @Test
@DisplayName("should return all counts when no filter provided") @DisplayName("should return all counts when no filter provided")
void shouldReturnAllCountsWhenNoFilter() { void shouldReturnAllCountsWhenNoFilter() {
when(inventoryCountRepository.findAll()).thenReturn(Result.success(List.of(count1, count2))); when(inventoryCountRepository.findAll(isNull(), eq(pageRequest)))
.thenReturn(Result.success(Page.of(List.of(count1, count2), 0, 100, 2)));
var result = listInventoryCounts.execute(null, null, actorId); var result = listInventoryCounts.execute(null, actorId, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
verify(inventoryCountRepository).findAll();
}
@Test
@DisplayName("should filter by storageLocationId")
void shouldFilterByStorageLocationId() {
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1")))
.thenReturn(Result.success(List.of(count1)));
var result = listInventoryCounts.execute("location-1", null, actorId);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1);
verify(inventoryCountRepository).findByStorageLocationId(StorageLocationId.of("location-1"));
verify(inventoryCountRepository, never()).findAll();
} }
@Test @Test
@DisplayName("should fail with RepositoryFailure when repository fails for findAll") @DisplayName("should fail with RepositoryFailure when repository fails for findAll")
void shouldFailWhenRepositoryFailsForFindAll() { void shouldFailWhenRepositoryFailsForFindAll() {
when(inventoryCountRepository.findAll()) when(inventoryCountRepository.findAll(isNull(), eq(pageRequest)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listInventoryCounts.execute(null, null, actorId); var result = listInventoryCounts.execute(null, actorId, pageRequest);
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 RepositoryFailure when repository fails for storageLocationId filter") @DisplayName("should return empty page when no counts match")
void shouldFailWhenRepositoryFailsForFilter() { void shouldReturnEmptyPageWhenNoCountsMatch() {
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1"))) when(inventoryCountRepository.findAll(eq(InventoryCountStatus.OPEN), eq(pageRequest)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.success(Page.empty(100)));
var result = listInventoryCounts.execute("location-1", null, actorId); var result = listInventoryCounts.execute("OPEN", actorId, pageRequest);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.RepositoryFailure.class);
}
@Test
@DisplayName("should return empty list when no counts match")
void shouldReturnEmptyListWhenNoCountsMatch() {
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("unknown")))
.thenReturn(Result.success(List.of()));
var result = listInventoryCounts.execute("unknown", null, actorId);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
}
@Test
@DisplayName("should fail with InvalidStorageLocationId for blank storageLocationId")
void shouldFailWhenBlankStorageLocationId() {
var result = listInventoryCounts.execute(" ", null, actorId);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStorageLocationId.class);
} }
@Test @Test
@DisplayName("should filter by status only") @DisplayName("should filter by status only")
void shouldFilterByStatusOnly() { void shouldFilterByStatusOnly() {
when(inventoryCountRepository.findByStatus(InventoryCountStatus.OPEN)) when(inventoryCountRepository.findAll(eq(InventoryCountStatus.OPEN), eq(pageRequest)))
.thenReturn(Result.success(List.of(count1))); .thenReturn(Result.success(Page.of(List.of(count1), 0, 100, 1)));
var result = listInventoryCounts.execute(null, "OPEN", actorId); var result = listInventoryCounts.execute("OPEN", actorId, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
verify(inventoryCountRepository).findByStatus(InventoryCountStatus.OPEN);
verify(inventoryCountRepository, never()).findAll();
}
@Test
@DisplayName("should filter by storageLocationId and status")
void shouldFilterByStorageLocationIdAndStatus() {
when(inventoryCountRepository.findByStorageLocationId(StorageLocationId.of("location-1")))
.thenReturn(Result.success(List.of(count1, count2)));
var result = listInventoryCounts.execute("location-1", "OPEN", actorId);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1);
assertThat(result.unsafeGetValue().getFirst().status()).isEqualTo(InventoryCountStatus.OPEN);
} }
@Test @Test
@DisplayName("should fail with InvalidStatus for invalid status string") @DisplayName("should fail with InvalidStatus for invalid status string")
void shouldFailWhenInvalidStatus() { void shouldFailWhenInvalidStatus() {
var result = listInventoryCounts.execute(null, "INVALID", actorId); var result = listInventoryCounts.execute("INVALID", actorId, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatus.class); assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.InvalidStatus.class);
@ -175,7 +130,7 @@ class ListInventoryCountsTest {
reset(authPort); reset(authPort);
when(authPort.can(any(ActorId.class), any())).thenReturn(false); when(authPort.can(any(ActorId.class), any())).thenReturn(false);
var result = listInventoryCounts.execute(null, null, actorId); var result = listInventoryCounts.execute(null, actorId, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.Unauthorized.class); assertThat(result.unsafeGetError()).isInstanceOf(InventoryCountError.Unauthorized.class);

View file

@ -2,6 +2,8 @@ package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -36,6 +38,7 @@ class ListStockMovementsTest {
private ListStockMovements useCase; private ListStockMovements useCase;
private StockMovement sampleMovement; private StockMovement sampleMovement;
private final ActorId actor = ActorId.of("user-1"); private final ActorId actor = ActorId.of("user-1");
private final PageRequest pageRequest = PageRequest.of(0, 100);
@BeforeEach @BeforeEach
void setUp() { void setUp() {
@ -62,33 +65,34 @@ class ListStockMovementsTest {
@Test @Test
@DisplayName("should return all movements when no filter") @DisplayName("should return all movements when no filter")
void shouldReturnAll() { void shouldReturnAll() {
when(repository.findAll()).thenReturn(Result.success(List.of(sampleMovement))); when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1)));
var result = useCase.execute(null, null, null, null, null, null, actor); var result = useCase.execute(null, null, null, null, null, null, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
verify(repository).findAll();
} }
@Test @Test
@DisplayName("should return empty list when no movements exist") @DisplayName("should return empty list when no movements exist")
void shouldReturnEmptyList() { void shouldReturnEmptyList() {
when(repository.findAll()).thenReturn(Result.success(List.of())); when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(Page.empty(100)));
var result = useCase.execute(null, null, null, null, null, null, actor); var result = useCase.execute(null, null, null, null, null, null, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
@Test @Test
@DisplayName("should fail when repository fails") @DisplayName("should fail when repository fails")
void shouldFailWhenRepositoryFails() { void shouldFailWhenRepositoryFails() {
when(repository.findAll()).thenReturn( when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = useCase.execute(null, null, null, null, null, null, actor); var result = useCase.execute(null, null, null, null, null, null, actor, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.RepositoryFailure.class);
@ -102,21 +106,19 @@ class ListStockMovementsTest {
@Test @Test
@DisplayName("should filter by stockId") @DisplayName("should filter by stockId")
void shouldFilterByStockId() { void shouldFilterByStockId() {
when(repository.findAllByStockId(StockId.of("stock-1"))) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of(sampleMovement))); .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1)));
var result = useCase.execute("stock-1", null, null, null, null, null, actor); var result = useCase.execute("stock-1", null, null, null, null, null, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
verify(repository).findAllByStockId(StockId.of("stock-1"));
verify(repository, never()).findAll();
} }
@Test @Test
@DisplayName("should fail with InvalidStockId when format invalid") @DisplayName("should fail with InvalidStockId when format invalid")
void shouldFailWhenStockIdInvalid() { void shouldFailWhenStockIdInvalid() {
var result = useCase.execute(" ", null, null, null, null, null, actor); var result = useCase.execute(" ", null, null, null, null, null, actor, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidStockId.class); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidStockId.class);
@ -130,20 +132,19 @@ class ListStockMovementsTest {
@Test @Test
@DisplayName("should filter by articleId") @DisplayName("should filter by articleId")
void shouldFilterByArticleId() { void shouldFilterByArticleId() {
when(repository.findAllByArticleId(ArticleId.of("article-1"))) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of(sampleMovement))); .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1)));
var result = useCase.execute(null, "article-1", null, null, null, null, actor); var result = useCase.execute(null, "article-1", null, null, null, null, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
verify(repository).findAllByArticleId(ArticleId.of("article-1"));
} }
@Test @Test
@DisplayName("should fail with InvalidArticleId when format invalid") @DisplayName("should fail with InvalidArticleId when format invalid")
void shouldFailWhenArticleIdInvalid() { void shouldFailWhenArticleIdInvalid() {
var result = useCase.execute(null, " ", null, null, null, null, actor); var result = useCase.execute(null, " ", null, null, null, null, actor, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidArticleId.class); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidArticleId.class);
@ -157,32 +158,31 @@ class ListStockMovementsTest {
@Test @Test
@DisplayName("should filter by batchReference") @DisplayName("should filter by batchReference")
void shouldFilterByBatchReference() { void shouldFilterByBatchReference() {
when(repository.findAllByBatchReference("CHARGE-001")) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of(sampleMovement))); .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1)));
var result = useCase.execute(null, null, null, "CHARGE-001", null, null, actor); var result = useCase.execute(null, null, null, "CHARGE-001", null, null, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
verify(repository).findAllByBatchReference("CHARGE-001");
} }
@Test @Test
@DisplayName("should return empty list when no movements for batch") @DisplayName("should return empty list when no movements for batch")
void shouldReturnEmptyForUnknownBatch() { void shouldReturnEmptyForUnknownBatch() {
when(repository.findAllByBatchReference("UNKNOWN")) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of())); .thenReturn(Result.success(Page.empty(100)));
var result = useCase.execute(null, null, null, "UNKNOWN", null, null, actor); var result = useCase.execute(null, null, null, "UNKNOWN", null, null, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
@Test @Test
@DisplayName("should fail with InvalidBatchReference when blank") @DisplayName("should fail with InvalidBatchReference when blank")
void shouldFailWhenBatchReferenceBlank() { void shouldFailWhenBatchReferenceBlank() {
var result = useCase.execute(null, null, null, " ", null, null, actor); var result = useCase.execute(null, null, null, " ", null, null, actor, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidBatchReference.class); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidBatchReference.class);
@ -191,10 +191,10 @@ class ListStockMovementsTest {
@Test @Test
@DisplayName("should fail when repository fails for batchReference") @DisplayName("should fail when repository fails for batchReference")
void shouldFailWhenRepositoryFailsForBatch() { void shouldFailWhenRepositoryFailsForBatch() {
when(repository.findAllByBatchReference("CHARGE-001")) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = useCase.execute(null, null, null, "CHARGE-001", null, null, actor); var result = useCase.execute(null, null, null, "CHARGE-001", null, null, actor, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.RepositoryFailure.class);
@ -208,20 +208,19 @@ class ListStockMovementsTest {
@Test @Test
@DisplayName("should filter by movementType") @DisplayName("should filter by movementType")
void shouldFilterByMovementType() { void shouldFilterByMovementType() {
when(repository.findAllByMovementType(MovementType.GOODS_RECEIPT)) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of(sampleMovement))); .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1)));
var result = useCase.execute(null, null, "GOODS_RECEIPT", null, null, null, actor); var result = useCase.execute(null, null, "GOODS_RECEIPT", null, null, null, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
verify(repository).findAllByMovementType(MovementType.GOODS_RECEIPT);
} }
@Test @Test
@DisplayName("should fail with InvalidMovementType when type invalid") @DisplayName("should fail with InvalidMovementType when type invalid")
void shouldFailWhenMovementTypeInvalid() { void shouldFailWhenMovementTypeInvalid() {
var result = useCase.execute(null, null, "INVALID_TYPE", null, null, null, actor); var result = useCase.execute(null, null, "INVALID_TYPE", null, null, null, actor, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidMovementType.class); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidMovementType.class);
@ -238,69 +237,64 @@ class ListStockMovementsTest {
@Test @Test
@DisplayName("should filter by from and to") @DisplayName("should filter by from and to")
void shouldFilterByFromAndTo() { void shouldFilterByFromAndTo() {
when(repository.findAllByPerformedAtBetween(from, to)) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of(sampleMovement))); .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1)));
var result = useCase.execute(null, null, null, null, from, to, actor); var result = useCase.execute(null, null, null, null, from, to, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
verify(repository).findAllByPerformedAtBetween(from, to);
} }
@Test @Test
@DisplayName("should filter with only from (open-ended)") @DisplayName("should filter with only from (open-ended)")
void shouldFilterByFromOnly() { void shouldFilterByFromOnly() {
when(repository.findAllByPerformedAtAfter(from)) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of(sampleMovement))); .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1)));
var result = useCase.execute(null, null, null, null, from, null, actor); var result = useCase.execute(null, null, null, null, from, null, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
verify(repository).findAllByPerformedAtAfter(from);
} }
@Test @Test
@DisplayName("should filter with only to (open-ended)") @DisplayName("should filter with only to (open-ended)")
void shouldFilterByToOnly() { void shouldFilterByToOnly() {
when(repository.findAllByPerformedAtBefore(to)) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of(sampleMovement))); .thenReturn(Result.success(Page.of(List.of(sampleMovement), 0, 100, 1)));
var result = useCase.execute(null, null, null, null, null, to, actor); var result = useCase.execute(null, null, null, null, null, to, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
verify(repository).findAllByPerformedAtBefore(to);
} }
@Test @Test
@DisplayName("should fail with InvalidDateRange when from is after to") @DisplayName("should fail with InvalidDateRange when from is after to")
void shouldFailWhenFromAfterTo() { void shouldFailWhenFromAfterTo() {
var result = useCase.execute(null, null, null, null, to, from, actor); var result = useCase.execute(null, null, null, null, to, from, actor, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidDateRange.class); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.InvalidDateRange.class);
verify(repository, never()).findAllByPerformedAtBetween(any(), any());
} }
@Test @Test
@DisplayName("should succeed when from equals to (same instant)") @DisplayName("should succeed when from equals to (same instant)")
void shouldSucceedWhenFromEqualsTo() { void shouldSucceedWhenFromEqualsTo() {
when(repository.findAllByPerformedAtBetween(from, from)) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of())); .thenReturn(Result.success(Page.empty(100)));
var result = useCase.execute(null, null, null, null, from, from, actor); var result = useCase.execute(null, null, null, null, from, from, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
verify(repository).findAllByPerformedAtBetween(from, from);
} }
@Test @Test
@DisplayName("should fail when repository fails for date range") @DisplayName("should fail when repository fails for date range")
void shouldFailWhenRepositoryFailsForDateRange() { void shouldFailWhenRepositoryFailsForDateRange() {
when(repository.findAllByPerformedAtBetween(from, to)) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = useCase.execute(null, null, null, null, from, to, actor); var result = useCase.execute(null, null, null, null, from, to, actor, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.RepositoryFailure.class);
@ -314,88 +308,73 @@ class ListStockMovementsTest {
@Test @Test
@DisplayName("stockId takes priority over articleId, batchReference and movementType") @DisplayName("stockId takes priority over articleId, batchReference and movementType")
void stockIdTakesPriority() { void stockIdTakesPriority() {
when(repository.findAllByStockId(StockId.of("stock-1"))) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of())); .thenReturn(Result.success(Page.empty(100)));
var result = useCase.execute("stock-1", "article-1", "GOODS_RECEIPT", "CHARGE-001", null, null, actor); var result = useCase.execute("stock-1", "article-1", "GOODS_RECEIPT", "CHARGE-001", null, null, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
verify(repository).findAllByStockId(StockId.of("stock-1"));
verify(repository, never()).findAllByArticleId(any());
verify(repository, never()).findAllByBatchReference(any());
verify(repository, never()).findAllByMovementType(any());
} }
@Test @Test
@DisplayName("articleId takes priority over batchReference and movementType") @DisplayName("articleId takes priority over batchReference and movementType")
void articleIdTakesPriorityOverBatchAndMovementType() { void articleIdTakesPriorityOverBatchAndMovementType() {
when(repository.findAllByArticleId(ArticleId.of("article-1"))) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of())); .thenReturn(Result.success(Page.empty(100)));
var result = useCase.execute(null, "article-1", "GOODS_RECEIPT", "CHARGE-001", null, null, actor); var result = useCase.execute(null, "article-1", "GOODS_RECEIPT", "CHARGE-001", null, null, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
verify(repository).findAllByArticleId(ArticleId.of("article-1"));
verify(repository, never()).findAllByBatchReference(any());
verify(repository, never()).findAllByMovementType(any());
} }
@Test @Test
@DisplayName("batchReference takes priority over movementType") @DisplayName("batchReference takes priority over movementType")
void batchReferenceTakesPriorityOverMovementType() { void batchReferenceTakesPriorityOverMovementType() {
when(repository.findAllByBatchReference("CHARGE-001")) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of())); .thenReturn(Result.success(Page.empty(100)));
var result = useCase.execute(null, null, "GOODS_RECEIPT", "CHARGE-001", null, null, actor); var result = useCase.execute(null, null, "GOODS_RECEIPT", "CHARGE-001", null, null, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
verify(repository).findAllByBatchReference("CHARGE-001");
verify(repository, never()).findAllByMovementType(any());
} }
@Test @Test
@DisplayName("movementType takes priority over from/to") @DisplayName("movementType takes priority over from/to")
void movementTypeTakesPriorityOverDateRange() { void movementTypeTakesPriorityOverDateRange() {
when(repository.findAllByMovementType(MovementType.GOODS_RECEIPT)) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of())); .thenReturn(Result.success(Page.empty(100)));
Instant from = Instant.parse("2026-01-01T00:00:00Z"); Instant from = Instant.parse("2026-01-01T00:00:00Z");
Instant to = Instant.parse("2026-12-31T23:59:59Z"); Instant to = Instant.parse("2026-12-31T23:59:59Z");
var result = useCase.execute(null, null, "GOODS_RECEIPT", null, from, to, actor); var result = useCase.execute(null, null, "GOODS_RECEIPT", null, from, to, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
verify(repository).findAllByMovementType(MovementType.GOODS_RECEIPT);
verify(repository, never()).findAllByPerformedAtBetween(any(), any());
} }
@Test @Test
@DisplayName("stockId takes priority over from/to") @DisplayName("stockId takes priority over from/to")
void stockIdTakesPriorityOverDateRange() { void stockIdTakesPriorityOverDateRange() {
when(repository.findAllByStockId(StockId.of("stock-1"))) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of())); .thenReturn(Result.success(Page.empty(100)));
Instant from = Instant.parse("2026-01-01T00:00:00Z"); Instant from = Instant.parse("2026-01-01T00:00:00Z");
Instant to = Instant.parse("2026-12-31T23:59:59Z"); Instant to = Instant.parse("2026-12-31T23:59:59Z");
var result = useCase.execute("stock-1", null, null, null, from, to, actor); var result = useCase.execute("stock-1", null, null, null, from, to, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
verify(repository).findAllByStockId(StockId.of("stock-1"));
verify(repository, never()).findAllByPerformedAtBetween(any(), any());
} }
@Test @Test
@DisplayName("batchReference takes priority over from/to") @DisplayName("batchReference takes priority over from/to")
void batchReferenceTakesPriorityOverDateRange() { void batchReferenceTakesPriorityOverDateRange() {
when(repository.findAllByBatchReference("CHARGE-001")) when(repository.findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class)))
.thenReturn(Result.success(List.of())); .thenReturn(Result.success(Page.empty(100)));
Instant from = Instant.parse("2026-01-01T00:00:00Z"); Instant from = Instant.parse("2026-01-01T00:00:00Z");
Instant to = Instant.parse("2026-12-31T23:59:59Z"); Instant to = Instant.parse("2026-12-31T23:59:59Z");
var result = useCase.execute(null, null, null, "CHARGE-001", from, to, actor); var result = useCase.execute(null, null, null, "CHARGE-001", from, to, actor, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
verify(repository).findAllByBatchReference("CHARGE-001");
verify(repository, never()).findAllByPerformedAtBetween(any(), any());
} }
} }
@ -408,11 +387,11 @@ class ListStockMovementsTest {
void shouldFailWhenUnauthorized() { void shouldFailWhenUnauthorized() {
when(authPort.can(actor, InventoryAction.STOCK_MOVEMENT_READ)).thenReturn(false); when(authPort.can(actor, InventoryAction.STOCK_MOVEMENT_READ)).thenReturn(false);
var result = useCase.execute(null, null, null, null, null, null, actor); var result = useCase.execute(null, null, null, null, null, null, actor, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.Unauthorized.class); assertThat(result.unsafeGetError()).isInstanceOf(StockMovementError.Unauthorized.class);
verify(repository, never()).findAll(); verify(repository, never()).findAll(any(), any(), any(), any(), any(), any(), any(PageRequest.class));
} }
} }
} }

View file

@ -2,6 +2,8 @@ package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -140,12 +142,16 @@ class ListStocksBelowMinimumTest {
// Unused methods for this test // Unused methods for this test
@Override public Result<RepositoryError, Optional<Stock>> findById(StockId id) { return Result.success(Optional.empty()); } @Override public Result<RepositoryError, Optional<Stock>> findById(StockId id) { return Result.success(Optional.empty()); }
@Override public Result<RepositoryError, Page<Stock>> findAll(PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); }
@Override public Result<RepositoryError, Page<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId, PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); }
@Override public Result<RepositoryError, Page<Stock>> findAllByArticleId(ArticleId articleId, PageRequest pageRequest) { return Result.success(Page.empty(pageRequest.size())); }
@Override public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(Optional.empty()); } @Override public Result<RepositoryError, Optional<Stock>> findByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(Optional.empty()); }
@Override public Result<RepositoryError, Boolean> existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(false); } @Override public Result<RepositoryError, Boolean> existsByArticleIdAndStorageLocationId(ArticleId articleId, StorageLocationId storageLocationId) { return Result.success(false); }
@Override public Result<RepositoryError, List<Stock>> findAll() { return Result.success(List.of()); } @Override public Result<RepositoryError, List<Stock>> findAll() { return Result.success(List.of()); }
@Override public Result<RepositoryError, List<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId) { return Result.success(List.of()); } @Override public Result<RepositoryError, List<Stock>> findAllByStorageLocationId(StorageLocationId storageLocationId) { return Result.success(List.of()); }
@Override public Result<RepositoryError, List<Stock>> findAllByArticleId(ArticleId articleId) { return Result.success(List.of()); } @Override public Result<RepositoryError, List<Stock>> findAllByArticleId(ArticleId articleId) { return Result.success(List.of()); }
@Override public Result<RepositoryError, List<Stock>> findAllWithExpiryRelevantBatches(LocalDate referenceDate) { return Result.success(List.of()); } @Override public Result<RepositoryError, List<Stock>> findAllWithExpiryRelevantBatches(LocalDate referenceDate) { return Result.success(List.of()); }
@Override public Result<RepositoryError, List<Stock>> findAllByBatchId(String batchId) { return Result.success(List.of()); }
@Override public Result<RepositoryError, Void> save(Stock stock) { return Result.success(null); } @Override public Result<RepositoryError, Void> save(Stock stock) { return Result.success(null); }
} }
} }

View file

@ -2,6 +2,8 @@ package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -14,6 +16,8 @@ import org.mockito.junit.jupiter.MockitoExtension;
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.ArgumentMatchers.eq;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -48,47 +52,51 @@ class ListStocksTest {
@Test @Test
@DisplayName("should return all stocks when no filter provided") @DisplayName("should return all stocks when no filter provided")
void shouldReturnAllStocksWhenNoFilter() { void shouldReturnAllStocksWhenNoFilter() {
when(stockRepository.findAll()).thenReturn(Result.success(List.of(stock1, stock2))); var pageRequest = PageRequest.of(0, 100);
when(stockRepository.findAll(any(PageRequest.class)))
.thenReturn(Result.success(Page.of(List.of(stock1, stock2), 0, 100, 2)));
var result = listStocks.execute(null, null); var result = listStocks.execute(null, null, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
verify(stockRepository).findAll(); verify(stockRepository).findAll(pageRequest);
} }
@Test @Test
@DisplayName("should filter by storageLocationId") @DisplayName("should filter by storageLocationId")
void shouldFilterByStorageLocationId() { void shouldFilterByStorageLocationId() {
when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1"))) var pageRequest = PageRequest.of(0, 100);
.thenReturn(Result.success(List.of(stock1, stock2))); when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1"), pageRequest))
.thenReturn(Result.success(Page.of(List.of(stock1, stock2), 0, 100, 2)));
var result = listStocks.execute("location-1", null); var result = listStocks.execute("location-1", null, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
verify(stockRepository).findAllByStorageLocationId(StorageLocationId.of("location-1")); verify(stockRepository).findAllByStorageLocationId(StorageLocationId.of("location-1"), pageRequest);
verify(stockRepository, never()).findAll(); verify(stockRepository, never()).findAll(any(PageRequest.class));
} }
@Test @Test
@DisplayName("should filter by articleId") @DisplayName("should filter by articleId")
void shouldFilterByArticleId() { void shouldFilterByArticleId() {
when(stockRepository.findAllByArticleId(ArticleId.of("article-1"))) var pageRequest = PageRequest.of(0, 100);
.thenReturn(Result.success(List.of(stock1))); when(stockRepository.findAllByArticleId(ArticleId.of("article-1"), pageRequest))
.thenReturn(Result.success(Page.of(List.of(stock1), 0, 100, 1)));
var result = listStocks.execute(null, "article-1"); var result = listStocks.execute(null, "article-1", pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
verify(stockRepository).findAllByArticleId(ArticleId.of("article-1")); verify(stockRepository).findAllByArticleId(ArticleId.of("article-1"), pageRequest);
verify(stockRepository, never()).findAll(); verify(stockRepository, never()).findAll(any(PageRequest.class));
} }
@Test @Test
@DisplayName("should fail when both filters are provided") @DisplayName("should fail when both filters are provided")
void shouldFailWhenBothFiltersProvided() { void shouldFailWhenBothFiltersProvided() {
var result = listStocks.execute("location-1", "article-1"); var result = listStocks.execute("location-1", "article-1", PageRequest.of(0, 100));
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidFilterCombination.class); assertThat(result.unsafeGetError()).isInstanceOf(StockError.InvalidFilterCombination.class);
@ -98,10 +106,11 @@ class ListStocksTest {
@Test @Test
@DisplayName("should fail with RepositoryFailure when repository fails for findAll") @DisplayName("should fail with RepositoryFailure when repository fails for findAll")
void shouldFailWhenRepositoryFailsForFindAll() { void shouldFailWhenRepositoryFailsForFindAll() {
when(stockRepository.findAll()) var pageRequest = PageRequest.of(0, 100);
when(stockRepository.findAll(any(PageRequest.class)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listStocks.execute(null, null); var result = listStocks.execute(null, null, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class);
@ -110,10 +119,11 @@ class ListStocksTest {
@Test @Test
@DisplayName("should fail with RepositoryFailure when repository fails for storageLocationId filter") @DisplayName("should fail with RepositoryFailure when repository fails for storageLocationId filter")
void shouldFailWhenRepositoryFailsForStorageLocationFilter() { void shouldFailWhenRepositoryFailsForStorageLocationFilter() {
when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("location-1"))) var pageRequest = PageRequest.of(0, 100);
when(stockRepository.findAllByStorageLocationId(eq(StorageLocationId.of("location-1")), any(PageRequest.class)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listStocks.execute("location-1", null); var result = listStocks.execute("location-1", null, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class);
@ -122,10 +132,11 @@ class ListStocksTest {
@Test @Test
@DisplayName("should fail with RepositoryFailure when repository fails for articleId filter") @DisplayName("should fail with RepositoryFailure when repository fails for articleId filter")
void shouldFailWhenRepositoryFailsForArticleIdFilter() { void shouldFailWhenRepositoryFailsForArticleIdFilter() {
when(stockRepository.findAllByArticleId(ArticleId.of("article-1"))) var pageRequest = PageRequest.of(0, 100);
when(stockRepository.findAllByArticleId(eq(ArticleId.of("article-1")), any(PageRequest.class)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listStocks.execute(null, "article-1"); var result = listStocks.execute(null, "article-1", pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(StockError.RepositoryFailure.class);
@ -134,12 +145,13 @@ class ListStocksTest {
@Test @Test
@DisplayName("should return empty list when no stocks match") @DisplayName("should return empty list when no stocks match")
void shouldReturnEmptyListWhenNoStocksMatch() { void shouldReturnEmptyListWhenNoStocksMatch() {
when(stockRepository.findAllByStorageLocationId(StorageLocationId.of("unknown"))) var pageRequest = PageRequest.of(0, 100);
.thenReturn(Result.success(List.of())); when(stockRepository.findAllByStorageLocationId(eq(StorageLocationId.of("unknown")), any(PageRequest.class)))
.thenReturn(Result.success(Page.empty(100)));
var result = listStocks.execute("unknown", null); var result = listStocks.execute("unknown", null, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
} }

View file

@ -1,6 +1,8 @@
package de.effigenix.application.inventory; package de.effigenix.application.inventory;
import de.effigenix.domain.inventory.*; import de.effigenix.domain.inventory.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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 org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -14,6 +16,9 @@ import org.mockito.junit.jupiter.MockitoExtension;
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.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -58,32 +63,37 @@ class ListStorageLocationsTest {
activeLocation("Lager A", StorageType.DRY_STORAGE), activeLocation("Lager A", StorageType.DRY_STORAGE),
inactiveLocation("Lager B", StorageType.COLD_ROOM) inactiveLocation("Lager B", StorageType.COLD_ROOM)
); );
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations)); var pageRequest = PageRequest.of(0, 100);
when(storageLocationRepository.findAll(isNull(), isNull(), eq(pageRequest)))
.thenReturn(Result.success(Page.of(locations, 0, 100, 2)));
var result = listStorageLocations.execute(null, null); var result = listStorageLocations.execute(null, null, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
} }
@Test @Test
@DisplayName("should return empty list when none exist") @DisplayName("should return empty list when none exist")
void shouldReturnEmptyList() { void shouldReturnEmptyList() {
when(storageLocationRepository.findAll()).thenReturn(Result.success(List.of())); var pageRequest = PageRequest.of(0, 100);
when(storageLocationRepository.findAll(isNull(), isNull(), eq(pageRequest)))
.thenReturn(Result.success(Page.empty(100)));
var result = listStorageLocations.execute(null, null); var result = listStorageLocations.execute(null, null, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
@Test @Test
@DisplayName("should fail when repository fails") @DisplayName("should fail when repository fails")
void shouldFailWhenRepositoryFails() { void shouldFailWhenRepositoryFails() {
when(storageLocationRepository.findAll()) var pageRequest = PageRequest.of(0, 100);
when(storageLocationRepository.findAll(isNull(), isNull(), eq(pageRequest)))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listStorageLocations.execute(null, null); var result = listStorageLocations.execute(null, null, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.RepositoryFailure.class);
@ -99,47 +109,44 @@ class ListStorageLocationsTest {
@Test @Test
@DisplayName("should return only active locations when active=true") @DisplayName("should return only active locations when active=true")
void shouldReturnOnlyActive() { void shouldReturnOnlyActive() {
var locations = List.of( var activeLocations = List.of(activeLocation("Aktiv", StorageType.DRY_STORAGE));
activeLocation("Aktiv", StorageType.DRY_STORAGE), var pageRequest = PageRequest.of(0, 100);
inactiveLocation("Inaktiv", StorageType.COLD_ROOM) when(storageLocationRepository.findAll(isNull(), eq(true), eq(pageRequest)))
); .thenReturn(Result.success(Page.of(activeLocations, 0, 100, 1)));
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations));
var result = listStorageLocations.execute(null, true); var result = listStorageLocations.execute(null, true, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Aktiv"); assertThat(result.unsafeGetValue().content().get(0).name().value()).isEqualTo("Aktiv");
} }
@Test @Test
@DisplayName("should return only inactive locations when active=false") @DisplayName("should return only inactive locations when active=false")
void shouldReturnOnlyInactive() { void shouldReturnOnlyInactive() {
var locations = List.of( var inactiveLocations = List.of(inactiveLocation("Inaktiv", StorageType.COLD_ROOM));
activeLocation("Aktiv", StorageType.DRY_STORAGE), var pageRequest = PageRequest.of(0, 100);
inactiveLocation("Inaktiv", StorageType.COLD_ROOM) when(storageLocationRepository.findAll(isNull(), eq(false), eq(pageRequest)))
); .thenReturn(Result.success(Page.of(inactiveLocations, 0, 100, 1)));
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations));
var result = listStorageLocations.execute(null, false); var result = listStorageLocations.execute(null, false, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Inaktiv"); assertThat(result.unsafeGetValue().content().get(0).name().value()).isEqualTo("Inaktiv");
} }
@Test @Test
@DisplayName("should return empty list when no locations match active filter") @DisplayName("should return empty list when no locations match active filter")
void shouldReturnEmptyWhenNoMatch() { void shouldReturnEmptyWhenNoMatch() {
var locations = List.of( var pageRequest = PageRequest.of(0, 100);
activeLocation("Aktiv", StorageType.DRY_STORAGE) when(storageLocationRepository.findAll(isNull(), eq(false), eq(pageRequest)))
); .thenReturn(Result.success(Page.empty(100)));
when(storageLocationRepository.findAll()).thenReturn(Result.success(locations));
var result = listStorageLocations.execute(null, false); var result = listStorageLocations.execute(null, false, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
} }
@ -152,23 +159,22 @@ class ListStorageLocationsTest {
@Test @Test
@DisplayName("should return locations of given storage type") @DisplayName("should return locations of given storage type")
void shouldReturnByStorageType() { void shouldReturnByStorageType() {
var coldRooms = List.of( var coldRooms = List.of(activeLocation("Kühlraum", StorageType.COLD_ROOM));
activeLocation("Kühlraum", StorageType.COLD_ROOM) var pageRequest = PageRequest.of(0, 100);
); when(storageLocationRepository.findAll(eq(StorageType.COLD_ROOM), isNull(), eq(pageRequest)))
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM)) .thenReturn(Result.success(Page.of(coldRooms, 0, 100, 1)));
.thenReturn(Result.success(coldRooms));
var result = listStorageLocations.execute("COLD_ROOM", null); var result = listStorageLocations.execute("COLD_ROOM", null, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
assertThat(result.unsafeGetValue().get(0).storageType()).isEqualTo(StorageType.COLD_ROOM); assertThat(result.unsafeGetValue().content().get(0).storageType()).isEqualTo(StorageType.COLD_ROOM);
} }
@Test @Test
@DisplayName("should fail with InvalidStorageType for unknown type") @DisplayName("should fail with InvalidStorageType for unknown type")
void shouldFailForInvalidStorageType() { void shouldFailForInvalidStorageType() {
var result = listStorageLocations.execute("INVALID", null); var result = listStorageLocations.execute("INVALID", null, PageRequest.of(0, 100));
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.InvalidStorageType.class); assertThat(result.unsafeGetError()).isInstanceOf(StorageLocationError.InvalidStorageType.class);
@ -185,50 +191,44 @@ class ListStorageLocationsTest {
@Test @Test
@DisplayName("should return only active locations of given type") @DisplayName("should return only active locations of given type")
void shouldReturnActiveOfType() { void shouldReturnActiveOfType() {
var coldRooms = List.of( var activeOfType = List.of(activeLocation("Kühl Aktiv", StorageType.COLD_ROOM));
activeLocation("Kühl Aktiv", StorageType.COLD_ROOM), var pageRequest = PageRequest.of(0, 100);
inactiveLocation("Kühl Inaktiv", StorageType.COLD_ROOM) when(storageLocationRepository.findAll(eq(StorageType.COLD_ROOM), eq(true), eq(pageRequest)))
); .thenReturn(Result.success(Page.of(activeOfType, 0, 100, 1)));
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM))
.thenReturn(Result.success(coldRooms));
var result = listStorageLocations.execute("COLD_ROOM", true); var result = listStorageLocations.execute("COLD_ROOM", true, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Kühl Aktiv"); assertThat(result.unsafeGetValue().content().get(0).name().value()).isEqualTo("Kühl Aktiv");
} }
@Test @Test
@DisplayName("should return only inactive locations of given type") @DisplayName("should return only inactive locations of given type")
void shouldReturnInactiveOfType() { void shouldReturnInactiveOfType() {
var coldRooms = List.of( var inactiveOfType = List.of(inactiveLocation("Kühl Inaktiv", StorageType.COLD_ROOM));
activeLocation("Kühl Aktiv", StorageType.COLD_ROOM), var pageRequest = PageRequest.of(0, 100);
inactiveLocation("Kühl Inaktiv", StorageType.COLD_ROOM) when(storageLocationRepository.findAll(eq(StorageType.COLD_ROOM), eq(false), eq(pageRequest)))
); .thenReturn(Result.success(Page.of(inactiveOfType, 0, 100, 1)));
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM))
.thenReturn(Result.success(coldRooms));
var result = listStorageLocations.execute("COLD_ROOM", false); var result = listStorageLocations.execute("COLD_ROOM", false, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
assertThat(result.unsafeGetValue().get(0).name().value()).isEqualTo("Kühl Inaktiv"); assertThat(result.unsafeGetValue().content().get(0).name().value()).isEqualTo("Kühl Inaktiv");
} }
@Test @Test
@DisplayName("should return empty list when no locations match combined filter") @DisplayName("should return empty list when no locations match combined filter")
void shouldReturnEmptyWhenNoMatchCombined() { void shouldReturnEmptyWhenNoMatchCombined() {
var coldRooms = List.of( var pageRequest = PageRequest.of(0, 100);
activeLocation("Kühl Aktiv", StorageType.COLD_ROOM) when(storageLocationRepository.findAll(eq(StorageType.COLD_ROOM), eq(false), eq(pageRequest)))
); .thenReturn(Result.success(Page.empty(100)));
when(storageLocationRepository.findByStorageType(StorageType.COLD_ROOM))
.thenReturn(Result.success(coldRooms));
var result = listStorageLocations.execute("COLD_ROOM", false); var result = listStorageLocations.execute("COLD_ROOM", false, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
} }
} }

View file

@ -11,6 +11,8 @@ import de.effigenix.domain.masterdata.article.*;
import de.effigenix.domain.masterdata.productcategory.ProductCategoryId; import de.effigenix.domain.masterdata.productcategory.ProductCategoryId;
import de.effigenix.domain.masterdata.supplier.SupplierId; import de.effigenix.domain.masterdata.supplier.SupplierId;
import de.effigenix.shared.common.Money; import de.effigenix.shared.common.Money;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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.persistence.UnitOfWork; import de.effigenix.shared.persistence.UnitOfWork;
@ -419,58 +421,37 @@ class ArticleUseCaseTest {
@DisplayName("should return all articles") @DisplayName("should return all articles")
void shouldReturnAllArticles() { void shouldReturnAllArticles() {
var articles = List.of(reconstitutedArticle("a-1"), reconstitutedArticle("a-2")); var articles = List.of(reconstitutedArticle("a-1"), reconstitutedArticle("a-2"));
when(articleRepository.findAll()).thenReturn(Result.success(articles)); var pageRequest = PageRequest.of(0, 100);
when(articleRepository.findAll(null, pageRequest))
.thenReturn(Result.success(Page.of(articles, 0, 100, 2)));
var result = listArticles.execute(); var result = listArticles.execute(null, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
} }
@Test @Test
@DisplayName("should return empty list when none exist") @DisplayName("should return empty list when none exist")
void shouldReturnEmptyList() { void shouldReturnEmptyList() {
when(articleRepository.findAll()).thenReturn(Result.success(List.of())); var pageRequest = PageRequest.of(0, 100);
when(articleRepository.findAll(null, pageRequest))
.thenReturn(Result.success(Page.empty(100)));
var result = listArticles.execute(); var result = listArticles.execute(null, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
@Test @Test
@DisplayName("should fail with RepositoryFailure when findAll fails") @DisplayName("should fail with RepositoryFailure when findAll fails")
void shouldFailWhenRepositoryFails() { void shouldFailWhenRepositoryFails() {
when(articleRepository.findAll()) var pageRequest = PageRequest.of(0, 100);
when(articleRepository.findAll(null, pageRequest))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listArticles.execute(); var result = listArticles.execute(null, pageRequest);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.RepositoryFailure.class);
}
@Test
@DisplayName("should return articles by category")
void shouldReturnByCategory() {
var catId = ProductCategoryId.of(CATEGORY_ID);
var articles = List.of(reconstitutedArticle("a-1"));
when(articleRepository.findByCategory(catId)).thenReturn(Result.success(articles));
var result = listArticles.executeByCategory(catId);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1);
}
@Test
@DisplayName("should fail with RepositoryFailure when findByCategory fails")
void shouldFailWhenFindByCategoryFails() {
var catId = ProductCategoryId.of(CATEGORY_ID);
when(articleRepository.findByCategory(catId))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listArticles.executeByCategory(catId);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.RepositoryFailure.class);
@ -479,23 +460,25 @@ class ArticleUseCaseTest {
@Test @Test
@DisplayName("should return articles by status") @DisplayName("should return articles by status")
void shouldReturnByStatus() { void shouldReturnByStatus() {
var pageRequest = PageRequest.of(0, 100);
var articles = List.of(reconstitutedArticle("a-1")); var articles = List.of(reconstitutedArticle("a-1"));
when(articleRepository.findByStatus(ArticleStatus.ACTIVE)) when(articleRepository.findAll(ArticleStatus.ACTIVE, pageRequest))
.thenReturn(Result.success(articles)); .thenReturn(Result.success(Page.of(articles, 0, 100, 1)));
var result = listArticles.executeByStatus(ArticleStatus.ACTIVE); var result = listArticles.execute(ArticleStatus.ACTIVE, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
} }
@Test @Test
@DisplayName("should fail with RepositoryFailure when findByStatus fails") @DisplayName("should fail with RepositoryFailure when findByStatus fails")
void shouldFailWhenFindByStatusFails() { void shouldFailWhenFindByStatusFails() {
when(articleRepository.findByStatus(ArticleStatus.ACTIVE)) var pageRequest = PageRequest.of(0, 100);
when(articleRepository.findAll(ArticleStatus.ACTIVE, pageRequest))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listArticles.executeByStatus(ArticleStatus.ACTIVE); var result = listArticles.execute(ArticleStatus.ACTIVE, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(ArticleError.RepositoryFailure.class);

View file

@ -5,7 +5,14 @@ import de.effigenix.application.masterdata.customer.command.*;
import de.effigenix.domain.masterdata.*; import de.effigenix.domain.masterdata.*;
import de.effigenix.domain.masterdata.article.ArticleId; import de.effigenix.domain.masterdata.article.ArticleId;
import de.effigenix.domain.masterdata.customer.*; import de.effigenix.domain.masterdata.customer.*;
import de.effigenix.shared.common.*; import de.effigenix.shared.common.Address;
import de.effigenix.shared.common.ContactInfo;
import de.effigenix.shared.common.Money;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.PaymentTerms;
import de.effigenix.shared.common.RepositoryError;
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.ActorId;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -386,81 +393,37 @@ class CustomerUseCaseTest {
@DisplayName("should return all customers") @DisplayName("should return all customers")
void shouldReturnAllCustomers() { void shouldReturnAllCustomers() {
var customers = List.of(existingB2BCustomer("c1"), existingB2CCustomer("c2")); var customers = List.of(existingB2BCustomer("c1"), existingB2CCustomer("c2"));
when(customerRepository.findAll()).thenReturn(Result.success(customers)); var pageRequest = PageRequest.of(0, 100);
when(customerRepository.findAll(pageRequest))
.thenReturn(Result.success(Page.of(customers, 0, 100, 2)));
var result = listCustomers.execute(); var result = listCustomers.execute(pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
} }
@Test @Test
@DisplayName("should return empty list when no customers exist") @DisplayName("should return empty list when no customers exist")
void shouldReturnEmptyList() { void shouldReturnEmptyList() {
when(customerRepository.findAll()).thenReturn(Result.success(List.of())); var pageRequest = PageRequest.of(0, 100);
when(customerRepository.findAll(pageRequest))
.thenReturn(Result.success(Page.empty(100)));
var result = listCustomers.execute(); var result = listCustomers.execute(pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
@Test @Test
@DisplayName("should fail with RepositoryFailure when findAll fails") @DisplayName("should fail with RepositoryFailure when findAll fails")
void shouldFailWhenFindAllFails() { void shouldFailWhenFindAllFails() {
when(customerRepository.findAll()) var pageRequest = PageRequest.of(0, 100);
when(customerRepository.findAll(pageRequest))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("timeout")));
var result = listCustomers.execute(); var result = listCustomers.execute(pageRequest);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should return customers by type")
void shouldReturnCustomersByType() {
var b2bCustomers = List.of(existingB2BCustomer("c1"));
when(customerRepository.findByType(CustomerType.B2B)).thenReturn(Result.success(b2bCustomers));
var result = listCustomers.executeByType(CustomerType.B2B);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1);
assertThat(result.unsafeGetValue().getFirst().type()).isEqualTo(CustomerType.B2B);
}
@Test
@DisplayName("should fail with RepositoryFailure when findByType fails")
void shouldFailWhenFindByTypeFails() {
when(customerRepository.findByType(CustomerType.B2B))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("error")));
var result = listCustomers.executeByType(CustomerType.B2B);
assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);
}
@Test
@DisplayName("should return customers by status")
void shouldReturnCustomersByStatus() {
var activeCustomers = List.of(existingB2BCustomer("c1"), existingB2CCustomer("c2"));
when(customerRepository.findByStatus(CustomerStatus.ACTIVE)).thenReturn(Result.success(activeCustomers));
var result = listCustomers.executeByStatus(CustomerStatus.ACTIVE);
assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2);
}
@Test
@DisplayName("should fail with RepositoryFailure when findByStatus fails")
void shouldFailWhenFindByStatusFails() {
when(customerRepository.findByStatus(CustomerStatus.ACTIVE))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("error")));
var result = listCustomers.executeByStatus(CustomerStatus.ACTIVE);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(CustomerError.RepositoryFailure.class);

View file

@ -9,6 +9,8 @@ import de.effigenix.application.masterdata.productcategory.UpdateProductCategory
import de.effigenix.domain.masterdata.article.Article; import de.effigenix.domain.masterdata.article.Article;
import de.effigenix.domain.masterdata.article.ArticleRepository; import de.effigenix.domain.masterdata.article.ArticleRepository;
import de.effigenix.domain.masterdata.productcategory.*; import de.effigenix.domain.masterdata.productcategory.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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.persistence.UnitOfWork; import de.effigenix.shared.persistence.UnitOfWork;
@ -346,32 +348,37 @@ class ProductCategoryUseCaseTest {
existingCategory("cat-1", "Backwaren"), existingCategory("cat-1", "Backwaren"),
existingCategory("cat-2", "Getränke") existingCategory("cat-2", "Getränke")
); );
when(categoryRepository.findAll()).thenReturn(Result.success(categories)); var pageRequest = PageRequest.of(0, 100);
when(categoryRepository.findAll(pageRequest))
.thenReturn(Result.success(Page.of(categories, 0, 100, 2)));
var result = useCase.execute(); var result = useCase.execute(pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
} }
@Test @Test
@DisplayName("should return empty list when no categories exist") @DisplayName("should return empty list when no categories exist")
void shouldReturnEmptyList() { void shouldReturnEmptyList() {
when(categoryRepository.findAll()).thenReturn(Result.success(List.of())); var pageRequest = PageRequest.of(0, 100);
when(categoryRepository.findAll(pageRequest))
.thenReturn(Result.success(Page.empty(100)));
var result = useCase.execute(); var result = useCase.execute(pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
@Test @Test
@DisplayName("should fail with RepositoryFailure when repository fails") @DisplayName("should fail with RepositoryFailure when repository fails")
void shouldFailWhenRepositoryFails() { void shouldFailWhenRepositoryFails() {
when(categoryRepository.findAll()) var pageRequest = PageRequest.of(0, 100);
when(categoryRepository.findAll(pageRequest))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = useCase.execute(); var result = useCase.execute(pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(ProductCategoryError.RepositoryFailure.class);

View file

@ -4,6 +4,8 @@ import de.effigenix.application.masterdata.supplier.*;
import de.effigenix.application.masterdata.supplier.command.*; import de.effigenix.application.masterdata.supplier.command.*;
import de.effigenix.domain.masterdata.supplier.*; import de.effigenix.domain.masterdata.supplier.*;
import de.effigenix.shared.common.ContactInfo; import de.effigenix.shared.common.ContactInfo;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
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.persistence.UnitOfWork; import de.effigenix.shared.persistence.UnitOfWork;
@ -388,32 +390,37 @@ class SupplierUseCaseTest {
@DisplayName("should return all suppliers") @DisplayName("should return all suppliers")
void shouldReturnAllSuppliers() { void shouldReturnAllSuppliers() {
var suppliers = List.of(existingSupplier("s-1"), existingSupplier("s-2")); var suppliers = List.of(existingSupplier("s-1"), existingSupplier("s-2"));
when(supplierRepository.findAll()).thenReturn(Result.success(suppliers)); var pageRequest = PageRequest.of(0, 100);
when(supplierRepository.findAll(null, pageRequest))
.thenReturn(Result.success(Page.of(suppliers, 0, 100, 2)));
var result = listSuppliers.execute(); var result = listSuppliers.execute(null, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
} }
@Test @Test
@DisplayName("should return empty list when no suppliers exist") @DisplayName("should return empty list when no suppliers exist")
void shouldReturnEmptyList() { void shouldReturnEmptyList() {
when(supplierRepository.findAll()).thenReturn(Result.success(List.of())); var pageRequest = PageRequest.of(0, 100);
when(supplierRepository.findAll(null, pageRequest))
.thenReturn(Result.success(Page.empty(100)));
var result = listSuppliers.execute(); var result = listSuppliers.execute(null, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
@Test @Test
@DisplayName("should fail with RepositoryFailure when findAll fails") @DisplayName("should fail with RepositoryFailure when findAll fails")
void shouldFailWhenFindAllFails() { void shouldFailWhenFindAllFails() {
when(supplierRepository.findAll()) var pageRequest = PageRequest.of(0, 100);
when(supplierRepository.findAll(null, pageRequest))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listSuppliers.execute(); var result = listSuppliers.execute(null, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);
@ -423,22 +430,24 @@ class SupplierUseCaseTest {
@DisplayName("should return suppliers filtered by status") @DisplayName("should return suppliers filtered by status")
void shouldReturnSuppliersByStatus() { void shouldReturnSuppliersByStatus() {
var activeSuppliers = List.of(existingSupplier("s-1")); var activeSuppliers = List.of(existingSupplier("s-1"));
when(supplierRepository.findByStatus(SupplierStatus.ACTIVE)) var pageRequest = PageRequest.of(0, 100);
.thenReturn(Result.success(activeSuppliers)); when(supplierRepository.findAll(SupplierStatus.ACTIVE, pageRequest))
.thenReturn(Result.success(Page.of(activeSuppliers, 0, 100, 1)));
var result = listSuppliers.executeByStatus(SupplierStatus.ACTIVE); var result = listSuppliers.execute(SupplierStatus.ACTIVE, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
} }
@Test @Test
@DisplayName("should fail with RepositoryFailure when findByStatus fails") @DisplayName("should fail with RepositoryFailure when findByStatus fails")
void shouldFailWhenFindByStatusFails() { void shouldFailWhenFindByStatusFails() {
when(supplierRepository.findByStatus(SupplierStatus.ACTIVE)) var pageRequest = PageRequest.of(0, 100);
when(supplierRepository.findAll(SupplierStatus.ACTIVE, pageRequest))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listSuppliers.executeByStatus(SupplierStatus.ACTIVE); var result = listSuppliers.execute(SupplierStatus.ACTIVE, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(SupplierError.RepositoryFailure.class);

View file

@ -1,6 +1,8 @@
package de.effigenix.application.production; package de.effigenix.application.production;
import de.effigenix.domain.production.*; import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -80,34 +82,39 @@ class ListBatchesTest {
@DisplayName("should return all batches") @DisplayName("should return all batches")
void should_ReturnAllBatches() { void should_ReturnAllBatches() {
var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED), sampleBatch("b2", BatchStatus.PLANNED)); var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED), sampleBatch("b2", BatchStatus.PLANNED));
var pageRequest = PageRequest.of(0, 100);
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true); when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findAllSummary()).thenReturn(Result.success(batches)); when(batchRepository.findAllSummary(pageRequest))
.thenReturn(Result.success(Page.of(batches, 0, 100, 2)));
var result = listBatches.execute(performedBy); var result = listBatches.execute(performedBy, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
} }
@Test @Test
@DisplayName("should return empty list when no batches exist") @DisplayName("should return empty list when no batches exist")
void should_ReturnEmptyList_When_NoBatchesExist() { void should_ReturnEmptyList_When_NoBatchesExist() {
var pageRequest = PageRequest.of(0, 100);
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true); when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findAllSummary()).thenReturn(Result.success(List.of())); when(batchRepository.findAllSummary(pageRequest))
.thenReturn(Result.success(Page.empty(100)));
var result = listBatches.execute(performedBy); var result = listBatches.execute(performedBy, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
@Test @Test
@DisplayName("should fail with RepositoryFailure when findAll fails") @DisplayName("should fail with RepositoryFailure when findAll fails")
void should_FailWithRepositoryFailure_When_FindAllFails() { void should_FailWithRepositoryFailure_When_FindAllFails() {
var pageRequest = PageRequest.of(0, 100);
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true); when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findAllSummary()).thenReturn(Result.failure(DB_ERROR)); when(batchRepository.findAllSummary(pageRequest)).thenReturn(Result.failure(DB_ERROR));
var result = listBatches.execute(performedBy); var result = listBatches.execute(performedBy, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(BatchError.RepositoryFailure.class);
@ -118,7 +125,7 @@ class ListBatchesTest {
void should_FailWithUnauthorized() { void should_FailWithUnauthorized() {
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false); when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(false);
var result = listBatches.execute(performedBy); var result = listBatches.execute(performedBy, PageRequest.of(0, 100));
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class); assertThat(result.unsafeGetError()).isInstanceOf(BatchError.Unauthorized.class);

View file

@ -1,6 +1,8 @@
package de.effigenix.application.production; package de.effigenix.application.production;
import de.effigenix.domain.production.*; import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.RepositoryError; import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
@ -61,14 +63,16 @@ class ListProductionOrdersTest {
@Test @Test
@DisplayName("should list all orders") @DisplayName("should list all orders")
void should_ListAll() { void should_ListAll() {
var pageRequest = PageRequest.of(0, 100);
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(true); when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(true);
when(productionOrderRepository.findAll()).thenReturn(Result.success(List.of(sampleOrder()))); when(productionOrderRepository.findAll(pageRequest))
.thenReturn(Result.success(Page.of(List.of(sampleOrder()), 0, 100, 1)));
var result = listProductionOrders.execute(performedBy); var result = listProductionOrders.execute(performedBy, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(1); assertThat(result.unsafeGetValue().content()).hasSize(1);
verify(productionOrderRepository).findAll(); verify(productionOrderRepository).findAll(pageRequest);
} }
@Test @Test
@ -118,7 +122,7 @@ class ListProductionOrdersTest {
void should_Fail_When_Unauthorized() { void should_Fail_When_Unauthorized() {
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(false); when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(false);
var result = listProductionOrders.execute(performedBy); var result = listProductionOrders.execute(performedBy, PageRequest.of(0, 100));
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.Unauthorized.class); assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.Unauthorized.class);
@ -128,11 +132,12 @@ class ListProductionOrdersTest {
@Test @Test
@DisplayName("should fail when repository returns error") @DisplayName("should fail when repository returns error")
void should_Fail_When_RepositoryError() { void should_Fail_When_RepositoryError() {
var pageRequest = PageRequest.of(0, 100);
when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(true); when(authPort.can(performedBy, ProductionAction.PRODUCTION_ORDER_READ)).thenReturn(true);
when(productionOrderRepository.findAll()) when(productionOrderRepository.findAll(pageRequest))
.thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost"))); .thenReturn(Result.failure(new RepositoryError.DatabaseError("connection lost")));
var result = listProductionOrders.execute(performedBy); var result = listProductionOrders.execute(performedBy, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.RepositoryFailure.class); assertThat(result.unsafeGetError()).isInstanceOf(ProductionOrderError.RepositoryFailure.class);

View file

@ -1,6 +1,8 @@
package de.effigenix.application.production; package de.effigenix.application.production;
import de.effigenix.domain.production.*; import de.effigenix.domain.production.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.Quantity;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure; import de.effigenix.shared.common.UnitOfMeasure;
@ -54,13 +56,15 @@ class ListRecipesTest {
recipeWithStatus("r1", RecipeStatus.DRAFT), recipeWithStatus("r1", RecipeStatus.DRAFT),
recipeWithStatus("r2", RecipeStatus.ACTIVE) recipeWithStatus("r2", RecipeStatus.ACTIVE)
); );
var pageRequest = PageRequest.of(0, 100);
when(authPort.can(performedBy, ProductionAction.RECIPE_READ)).thenReturn(true); when(authPort.can(performedBy, ProductionAction.RECIPE_READ)).thenReturn(true);
when(recipeRepository.findAll()).thenReturn(Result.success(recipes)); when(recipeRepository.findAll(pageRequest))
.thenReturn(Result.success(Page.of(recipes, 0, 100, 2)));
var result = listRecipes.execute(performedBy); var result = listRecipes.execute(performedBy, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
} }
@Test @Test
@ -82,7 +86,7 @@ class ListRecipesTest {
void should_FailWithUnauthorized_When_ActorLacksPermission() { void should_FailWithUnauthorized_When_ActorLacksPermission() {
when(authPort.can(performedBy, ProductionAction.RECIPE_READ)).thenReturn(false); when(authPort.can(performedBy, ProductionAction.RECIPE_READ)).thenReturn(false);
var result = listRecipes.execute(performedBy); var result = listRecipes.execute(performedBy, PageRequest.of(0, 100));
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.Unauthorized.class); assertThat(result.unsafeGetError()).isInstanceOf(RecipeError.Unauthorized.class);

View file

@ -2,6 +2,8 @@ package de.effigenix.application.shared;
import de.effigenix.shared.common.Country; import de.effigenix.shared.common.Country;
import de.effigenix.shared.common.CountryRepository; import de.effigenix.shared.common.CountryRepository;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
@ -23,12 +25,45 @@ class ListCountriesTest {
@InjectMocks @InjectMocks
private ListCountries listCountries; private ListCountries listCountries;
@Test
void shouldDelegateToFindAllWithPageRequest() {
var expected = List.of(new Country("DE", "Deutschland"));
var pageRequest = PageRequest.of(0, 100);
when(countryRepository.findAll(pageRequest))
.thenReturn(Page.of(expected, 0, 100, 1));
var result = listCountries.execute(pageRequest);
assertThat(result.content()).isEqualTo(expected);
verify(countryRepository).findAll(pageRequest);
}
@Test
void shouldDelegateToSearchWhenQueryIsProvided() {
var expected = List.of(new Country("DE", "Deutschland"));
when(countryRepository.search("deutsch")).thenReturn(expected);
var result = listCountries.search("deutsch");
assertThat(result).isEqualTo(expected);
verify(countryRepository).search("deutsch");
}
@Test
void shouldReturnEmptyListWhenNoMatch() {
when(countryRepository.search("xyz")).thenReturn(List.of());
var result = listCountries.search("xyz");
assertThat(result).isEmpty();
}
@Test @Test
void shouldDelegateToFindAllWhenQueryIsNull() { void shouldDelegateToFindAllWhenQueryIsNull() {
var expected = List.of(new Country("DE", "Deutschland")); var expected = List.of(new Country("DE", "Deutschland"));
when(countryRepository.findAll()).thenReturn(expected); when(countryRepository.findAll()).thenReturn(expected);
var result = listCountries.execute(null); var result = listCountries.search(null);
assertThat(result).isEqualTo(expected); assertThat(result).isEqualTo(expected);
verify(countryRepository).findAll(); verify(countryRepository).findAll();
@ -39,38 +74,18 @@ class ListCountriesTest {
var expected = List.of(new Country("DE", "Deutschland")); var expected = List.of(new Country("DE", "Deutschland"));
when(countryRepository.findAll()).thenReturn(expected); when(countryRepository.findAll()).thenReturn(expected);
var result = listCountries.execute(" "); var result = listCountries.search(" ");
assertThat(result).isEqualTo(expected); assertThat(result).isEqualTo(expected);
verify(countryRepository).findAll(); verify(countryRepository).findAll();
} }
@Test
void shouldDelegateToSearchWhenQueryIsProvided() {
var expected = List.of(new Country("DE", "Deutschland"));
when(countryRepository.search("deutsch")).thenReturn(expected);
var result = listCountries.execute("deutsch");
assertThat(result).isEqualTo(expected);
verify(countryRepository).search("deutsch");
}
@Test
void shouldReturnEmptyListWhenNoMatch() {
when(countryRepository.search("xyz")).thenReturn(List.of());
var result = listCountries.execute("xyz");
assertThat(result).isEmpty();
}
@Test @Test
void shouldDelegateToFindAllWhenQueryIsEmpty() { void shouldDelegateToFindAllWhenQueryIsEmpty() {
var expected = List.of(new Country("DE", "Deutschland")); var expected = List.of(new Country("DE", "Deutschland"));
when(countryRepository.findAll()).thenReturn(expected); when(countryRepository.findAll()).thenReturn(expected);
var result = listCountries.execute(""); var result = listCountries.search("");
assertThat(result).isEqualTo(expected); assertThat(result).isEqualTo(expected);
verify(countryRepository).findAll(); verify(countryRepository).findAll();

View file

@ -2,6 +2,8 @@ package de.effigenix.application.usermanagement;
import de.effigenix.application.usermanagement.dto.UserDTO; import de.effigenix.application.usermanagement.dto.UserDTO;
import de.effigenix.domain.usermanagement.*; import de.effigenix.domain.usermanagement.*;
import de.effigenix.shared.common.Page;
import de.effigenix.shared.common.PageRequest;
import de.effigenix.shared.common.Result; import de.effigenix.shared.common.Result;
import de.effigenix.shared.security.ActorId; import de.effigenix.shared.security.ActorId;
import de.effigenix.shared.security.AuthorizationPort; import de.effigenix.shared.security.AuthorizationPort;
@ -32,6 +34,7 @@ class ListUsersTest {
private ActorId performedBy; private ActorId performedBy;
private User user1; private User user1;
private User user2; private User user2;
private final PageRequest pageRequest = PageRequest.of(0, 100);
@BeforeEach @BeforeEach
void setUp() { void setUp() {
@ -53,13 +56,14 @@ class ListUsersTest {
@DisplayName("should_ReturnAllUsers_When_Authorized") @DisplayName("should_ReturnAllUsers_When_Authorized")
void should_ReturnAllUsers_When_Authorized() { void should_ReturnAllUsers_When_Authorized() {
when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true); when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true);
when(userRepository.findAll()).thenReturn(Result.success(List.of(user1, user2))); when(userRepository.findAll(null, pageRequest))
.thenReturn(Result.success(Page.of(List.of(user1, user2), 0, 100, 2)));
Result<UserError, List<UserDTO>> result = listUsers.execute(performedBy); Result<UserError, Page<UserDTO>> result = listUsers.execute(null, performedBy, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
assertThat(result.unsafeGetValue()).extracting(UserDTO::username) assertThat(result.unsafeGetValue().content()).extracting(UserDTO::username)
.containsExactlyInAnyOrder("john.doe", "jane.doe"); .containsExactlyInAnyOrder("john.doe", "jane.doe");
} }
@ -67,12 +71,13 @@ class ListUsersTest {
@DisplayName("should_ReturnEmptyList_When_NoUsersExist") @DisplayName("should_ReturnEmptyList_When_NoUsersExist")
void should_ReturnEmptyList_When_NoUsersExist() { void should_ReturnEmptyList_When_NoUsersExist() {
when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true); when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true);
when(userRepository.findAll()).thenReturn(Result.success(List.of())); when(userRepository.findAll(null, pageRequest))
.thenReturn(Result.success(Page.empty(100)));
Result<UserError, List<UserDTO>> result = listUsers.execute(performedBy); Result<UserError, Page<UserDTO>> result = listUsers.execute(null, performedBy, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).isEmpty(); assertThat(result.unsafeGetValue().content()).isEmpty();
} }
@Test @Test
@ -80,23 +85,24 @@ class ListUsersTest {
void should_FailWithUnauthorized_When_ActorLacksPermission() { void should_FailWithUnauthorized_When_ActorLacksPermission() {
when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(false); when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(false);
Result<UserError, List<UserDTO>> result = listUsers.execute(performedBy); Result<UserError, Page<UserDTO>> result = listUsers.execute(null, performedBy, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class); assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).findAll(); verify(userRepository, never()).findAll(any(), any());
} }
@Test @Test
@DisplayName("should_ReturnBranchUsers_When_FilteredByBranch") @DisplayName("should_ReturnBranchUsers_When_FilteredByBranch")
void should_ReturnBranchUsers_When_FilteredByBranch() { void should_ReturnBranchUsers_When_FilteredByBranch() {
when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true); when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(true);
when(userRepository.findByBranchId("branch-1")).thenReturn(Result.success(List.of(user1, user2))); when(userRepository.findAll("branch-1", pageRequest))
.thenReturn(Result.success(Page.of(List.of(user1, user2), 0, 100, 2)));
Result<UserError, List<UserDTO>> result = listUsers.executeForBranch(BranchId.of("branch-1"), performedBy); Result<UserError, Page<UserDTO>> result = listUsers.execute("branch-1", performedBy, pageRequest);
assertThat(result.isSuccess()).isTrue(); assertThat(result.isSuccess()).isTrue();
assertThat(result.unsafeGetValue()).hasSize(2); assertThat(result.unsafeGetValue().content()).hasSize(2);
} }
@Test @Test
@ -104,10 +110,10 @@ class ListUsersTest {
void should_FailWithUnauthorized_When_ActorLacksPermissionForBranchList() { void should_FailWithUnauthorized_When_ActorLacksPermissionForBranchList() {
when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(false); when(authPort.can(performedBy, UserManagementAction.USER_LIST)).thenReturn(false);
Result<UserError, List<UserDTO>> result = listUsers.executeForBranch(BranchId.of("branch-1"), performedBy); Result<UserError, Page<UserDTO>> result = listUsers.execute("branch-1", performedBy, pageRequest);
assertThat(result.isFailure()).isTrue(); assertThat(result.isFailure()).isTrue();
assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class); assertThat(result.unsafeGetError()).isInstanceOf(UserError.Unauthorized.class);
verify(userRepository, never()).findByBranchId(anyString()); verify(userRepository, never()).findAll(any(), any());
} }
} }

View file

@ -2,10 +2,7 @@ package de.effigenix.domain.production;
import com.code_intelligence.jazzer.api.FuzzedDataProvider; import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.junit.FuzzTest; import com.code_intelligence.jazzer.junit.FuzzTest;
import de.effigenix.shared.common.Quantity; import de.effigenix.shared.common.*;
import de.effigenix.shared.common.RepositoryError;
import de.effigenix.shared.common.Result;
import de.effigenix.shared.common.UnitOfMeasure;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
@ -237,6 +234,7 @@ class BatchTraceabilityServiceFuzzTest {
@Override public Result<RepositoryError, List<Batch>> findByStatus(BatchStatus s) { throw new UnsupportedOperationException(); } @Override public Result<RepositoryError, List<Batch>> findByStatus(BatchStatus s) { throw new UnsupportedOperationException(); }
@Override public Result<RepositoryError, List<Batch>> findByProductionDate(LocalDate d) { throw new UnsupportedOperationException(); } @Override public Result<RepositoryError, List<Batch>> findByProductionDate(LocalDate d) { throw new UnsupportedOperationException(); }
@Override public Result<RepositoryError, List<Batch>> findByRecipeIds(List<RecipeId> ids) { throw new UnsupportedOperationException(); } @Override public Result<RepositoryError, List<Batch>> findByRecipeIds(List<RecipeId> ids) { throw new UnsupportedOperationException(); }
@Override public Result<RepositoryError, Page<Batch>> findAllSummary(PageRequest pr) { throw new UnsupportedOperationException(); }
@Override public Result<RepositoryError, List<Batch>> findAllSummary() { throw new UnsupportedOperationException(); } @Override public Result<RepositoryError, List<Batch>> findAllSummary() { throw new UnsupportedOperationException(); }
@Override public Result<RepositoryError, List<Batch>> findByStatusSummary(BatchStatus s) { throw new UnsupportedOperationException(); } @Override public Result<RepositoryError, List<Batch>> findByStatusSummary(BatchStatus s) { throw new UnsupportedOperationException(); }
@Override public Result<RepositoryError, List<Batch>> findByProductionDateSummary(LocalDate d) { throw new UnsupportedOperationException(); } @Override public Result<RepositoryError, List<Batch>> findByProductionDateSummary(LocalDate d) { throw new UnsupportedOperationException(); }

View file

@ -160,7 +160,7 @@ class InventoryCountControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/inventory/inventory-counts") mockMvc.perform(get("/api/inventory/inventory-counts")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(1)); .andExpect(jsonPath("$.content.length()").value(1));
} }
@Test @Test
@ -175,11 +175,10 @@ class InventoryCountControllerIntegrationTest extends AbstractIntegrationTest {
.andExpect(status().isCreated()); .andExpect(status().isCreated());
mockMvc.perform(get("/api/inventory/inventory-counts") mockMvc.perform(get("/api/inventory/inventory-counts")
.param("storageLocationId", storageLocationId)
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(1)) .andExpect(jsonPath("$.content.length()").value(1))
.andExpect(jsonPath("$[0].storageLocationId").value(storageLocationId)); .andExpect(jsonPath("$.content[0].storageLocationId").value(storageLocationId));
} }
// ==================== Security ==================== // ==================== Security ====================
@ -298,7 +297,7 @@ class InventoryCountControllerIntegrationTest extends AbstractIntegrationTest {
.param("storageLocationId", UUID.randomUUID().toString()) .param("storageLocationId", UUID.randomUUID().toString())
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(0)); .andExpect(jsonPath("$.content.length()").value(0));
} }
// ==================== US-6.2: Inventur starten ==================== // ==================== US-6.2: Inventur starten ====================
@ -554,8 +553,8 @@ class InventoryCountControllerIntegrationTest extends AbstractIntegrationTest {
.param("movementType", "ADJUSTMENT") .param("movementType", "ADJUSTMENT")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(org.hamcrest.Matchers.greaterThanOrEqualTo(1))) .andExpect(jsonPath("$.content.length()").value(org.hamcrest.Matchers.greaterThanOrEqualTo(1)))
.andExpect(jsonPath("$[0].movementType").value("ADJUSTMENT")); .andExpect(jsonPath("$.content[0].movementType").value("ADJUSTMENT"));
} }
@Test @Test
@ -783,8 +782,8 @@ class InventoryCountControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/inventory/inventory-counts?status=OPEN") mockMvc.perform(get("/api/inventory/inventory-counts?status=OPEN")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(1)) .andExpect(jsonPath("$.content.length()").value(1))
.andExpect(jsonPath("$[0].status").value("OPEN")); .andExpect(jsonPath("$.content[0].status").value("OPEN"));
} }
@Test @Test

View file

@ -924,7 +924,7 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/inventory/stocks") mockMvc.perform(get("/api/inventory/stocks")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2)); .andExpect(jsonPath("$.content.length()").value(2));
} }
@Test @Test
@ -938,8 +938,8 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest {
.param("storageLocationId", storageLocationId) .param("storageLocationId", storageLocationId)
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(1)) .andExpect(jsonPath("$.content.length()").value(1))
.andExpect(jsonPath("$[0].storageLocationId").value(storageLocationId)); .andExpect(jsonPath("$.content[0].storageLocationId").value(storageLocationId));
} }
@Test @Test
@ -952,8 +952,8 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest {
.param("articleId", articleId) .param("articleId", articleId)
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(1)) .andExpect(jsonPath("$.content.length()").value(1))
.andExpect(jsonPath("$[0].articleId").value(articleId)); .andExpect(jsonPath("$.content[0].articleId").value(articleId));
} }
@Test @Test
@ -962,7 +962,7 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/inventory/stocks") mockMvc.perform(get("/api/inventory/stocks")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(0)); .andExpect(jsonPath("$.content.length()").value(0));
} }
@Test @Test
@ -974,9 +974,9 @@ class StockControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/inventory/stocks") mockMvc.perform(get("/api/inventory/stocks")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$[0].batches.length()").value(1)) .andExpect(jsonPath("$.content[0].batches.length()").value(1))
.andExpect(jsonPath("$[0].totalQuantity").value(10)) .andExpect(jsonPath("$.content[0].totalQuantity").value(10))
.andExpect(jsonPath("$[0].availableQuantity").value(10)); .andExpect(jsonPath("$.content[0].availableQuantity").value(10));
} }
@Test @Test

View file

@ -407,8 +407,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/inventory/stock-movements") mockMvc.perform(get("/api/inventory/stock-movements")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.length()").value(1)); .andExpect(jsonPath("$.content.length()").value(1));
} }
@Test @Test
@ -420,7 +420,7 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
.param("stockId", stockId) .param("stockId", stockId)
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()); .andExpect(jsonPath("$.content").isArray());
} }
@Test @Test
@ -432,7 +432,7 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
.param("movementType", "GOODS_RECEIPT") .param("movementType", "GOODS_RECEIPT")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()); .andExpect(jsonPath("$.content").isArray());
} }
@Test @Test
@ -444,8 +444,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
.param("articleId", articleId) .param("articleId", articleId)
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.length()").value(1)); .andExpect(jsonPath("$.content.length()").value(1));
} }
@Test @Test
@ -472,8 +472,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/inventory/stock-movements") mockMvc.perform(get("/api/inventory/stock-movements")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.length()").value(0)); .andExpect(jsonPath("$.content.length()").value(0));
} }
@Test @Test
@ -485,8 +485,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
.param("batchReference", "CHARGE-001") .param("batchReference", "CHARGE-001")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.length()").value(1)); .andExpect(jsonPath("$.content.length()").value(1));
} }
@Test @Test
@ -499,8 +499,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
.param("to", "2030-12-31T23:59:59Z") .param("to", "2030-12-31T23:59:59Z")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.length()").value(1)); .andExpect(jsonPath("$.content.length()").value(1));
} }
@Test @Test
@ -512,8 +512,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
.param("from", "2020-01-01T00:00:00Z") .param("from", "2020-01-01T00:00:00Z")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.length()").value(1)); .andExpect(jsonPath("$.content.length()").value(1));
} }
@Test @Test
@ -525,8 +525,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
.param("to", "2030-12-31T23:59:59Z") .param("to", "2030-12-31T23:59:59Z")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.length()").value(1)); .andExpect(jsonPath("$.content.length()").value(1));
} }
@Test @Test
@ -550,7 +550,7 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
.param("batchReference", "CHARGE-001") .param("batchReference", "CHARGE-001")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()); .andExpect(jsonPath("$.content").isArray());
} }
@Test @Test
@ -572,8 +572,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
.param("batchReference", "UNKNOWN-CHARGE") .param("batchReference", "UNKNOWN-CHARGE")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.length()").value(0)); .andExpect(jsonPath("$.content.length()").value(0));
} }
@Test @Test
@ -586,8 +586,8 @@ class StockMovementControllerIntegrationTest extends AbstractIntegrationTest {
.param("to", "2099-12-31T23:59:59Z") .param("to", "2099-12-31T23:59:59Z")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$").isArray()) .andExpect(jsonPath("$.content").isArray())
.andExpect(jsonPath("$.length()").value(0)); .andExpect(jsonPath("$.content.length()").value(0));
} }
} }

View file

@ -500,11 +500,11 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/inventory/storage-locations") mockMvc.perform(get("/api/inventory/storage-locations")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2))) .andExpect(jsonPath("$.content", hasSize(2)))
.andExpect(jsonPath("$[*].name", containsInAnyOrder("Lager A", "Lager B"))) .andExpect(jsonPath("$.content[*].name", containsInAnyOrder("Lager A", "Lager B")))
.andExpect(jsonPath("$[0].id").isNotEmpty()) .andExpect(jsonPath("$.content[0].id").isNotEmpty())
.andExpect(jsonPath("$[0].storageType").isNotEmpty()) .andExpect(jsonPath("$.content[0].storageType").isNotEmpty())
.andExpect(jsonPath("$[0].active").isBoolean()); .andExpect(jsonPath("$.content[0].active").isBoolean());
} }
@Test @Test
@ -518,8 +518,8 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
.param("storageType", "COLD_ROOM") .param("storageType", "COLD_ROOM")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$[0].storageType").value("COLD_ROOM")); .andExpect(jsonPath("$.content[0].storageType").value("COLD_ROOM"));
} }
@Test @Test
@ -537,8 +537,8 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
.param("active", "true") .param("active", "true")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$[0].name").value("Aktiv")); .andExpect(jsonPath("$.content[0].name").value("Aktiv"));
} }
@Test @Test
@ -555,8 +555,8 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
.param("active", "false") .param("active", "false")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$[0].name").value("Inaktiv")); .andExpect(jsonPath("$.content[0].name").value("Inaktiv"));
} }
@Test @Test
@ -575,8 +575,8 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
.param("active", "true") .param("active", "true")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$[0].name").value("Kühl Aktiv")); .andExpect(jsonPath("$.content[0].name").value("Kühl Aktiv"));
} }
@Test @Test
@ -595,8 +595,8 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
.param("active", "false") .param("active", "false")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$[0].name").value("Kühl Inaktiv")); .andExpect(jsonPath("$.content[0].name").value("Kühl Inaktiv"));
} }
@Test @Test
@ -617,7 +617,7 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/inventory/storage-locations") mockMvc.perform(get("/api/inventory/storage-locations")
.header("Authorization", "Bearer " + readerToken)) .header("Authorization", "Bearer " + readerToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1))); .andExpect(jsonPath("$.content", hasSize(1)));
} }
@Test @Test
@ -641,7 +641,7 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/inventory/storage-locations") mockMvc.perform(get("/api/inventory/storage-locations")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(0))); .andExpect(jsonPath("$.content", hasSize(0)));
} }
@Test @Test
@ -652,12 +652,12 @@ class StorageLocationControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/inventory/storage-locations") mockMvc.perform(get("/api/inventory/storage-locations")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").isNotEmpty()) .andExpect(jsonPath("$.content[0].id").isNotEmpty())
.andExpect(jsonPath("$[0].name").value("Vollständig")) .andExpect(jsonPath("$.content[0].name").value("Vollständig"))
.andExpect(jsonPath("$[0].storageType").value("COLD_ROOM")) .andExpect(jsonPath("$.content[0].storageType").value("COLD_ROOM"))
.andExpect(jsonPath("$[0].temperatureRange.minTemperature").value(-2)) .andExpect(jsonPath("$.content[0].temperatureRange.minTemperature").value(-2))
.andExpect(jsonPath("$[0].temperatureRange.maxTemperature").value(8)) .andExpect(jsonPath("$.content[0].temperatureRange.maxTemperature").value(8))
.andExpect(jsonPath("$[0].active").value(true)); .andExpect(jsonPath("$.content[0].active").value(true));
} }
// ==================== Hilfsmethoden ==================== // ==================== Hilfsmethoden ====================

View file

@ -181,7 +181,7 @@ class ArticleControllerIntegrationTest extends AbstractIntegrationTest {
.param("status", "ACTIVE") .param("status", "ACTIVE")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$[*].status", everyItem(is("ACTIVE")))); .andExpect(jsonPath("$.content[*].status", everyItem(is("ACTIVE"))));
} }
// ==================== TC-ART-07: Verkaufseinheit hinzufügen ==================== // ==================== TC-ART-07: Verkaufseinheit hinzufügen ====================
@ -372,21 +372,19 @@ class ArticleControllerIntegrationTest extends AbstractIntegrationTest {
.andExpect(jsonPath("$.supplierIds", hasSize(0))); .andExpect(jsonPath("$.supplierIds", hasSize(0)));
} }
// ==================== Artikel nach Kategorie filtern ==================== // ==================== Artikel auflisten ====================
@Test @Test
@DisplayName("Artikel nach categoryId filtern → nur passende zurückgegeben") @DisplayName("Alle Artikel auflisten → alle vorhanden")
void filterByCategory_returnsOnlyMatching() throws Exception { void listArticles_returnsAll() throws Exception {
String otherCategoryId = createCategory("Milchprodukte");
createArticle("Äpfel", "FI-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99"); createArticle("Äpfel", "FI-001", Unit.PIECE_FIXED, PriceModel.FIXED, "1.99");
String otherCategoryId = createCategory("Milchprodukte");
createArticleInCategory("Milch", "MI-001", Unit.KG, PriceModel.WEIGHT_BASED, "1.29", otherCategoryId); createArticleInCategory("Milch", "MI-001", Unit.KG, PriceModel.WEIGHT_BASED, "1.29", otherCategoryId);
mockMvc.perform(get("/api/articles") mockMvc.perform(get("/api/articles")
.param("categoryId", otherCategoryId)
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$.content", hasSize(2)));
.andExpect(jsonPath("$[0].name").value("Milch"));
} }
// ==================== Artikel aktualisieren ==================== // ==================== Artikel aktualisieren ====================

View file

@ -181,45 +181,15 @@ class CustomerControllerIntegrationTest extends AbstractIntegrationTest {
// ==================== TC-CUS-06: Filter ==================== // ==================== TC-CUS-06: Filter ====================
@Test @Test
@DisplayName("TC-CUS-06: Nur B2B-Kunden filtern → nur B2B") @DisplayName("TC-CUS-06: Alle Kunden auflisten → verschiedene Typen enthalten")
void listCustomers_filterByB2B_returnsOnlyB2B() throws Exception { void listCustomers_returnsAll() throws Exception {
createB2bCustomer("Gastro GmbH"); createB2bCustomer("Gastro GmbH");
createB2cCustomer("Max Mustermann"); createB2cCustomer("Max Mustermann");
mockMvc.perform(get("/api/customers") mockMvc.perform(get("/api/customers")
.param("type", "B2B")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$[*].type", everyItem(is("B2B")))); .andExpect(jsonPath("$.content", hasSize(greaterThanOrEqualTo(2))));
}
@Test
@DisplayName("TC-CUS-06: Nur B2C-Kunden filtern → nur B2C")
void listCustomers_filterByB2C_returnsOnlyB2C() throws Exception {
createB2bCustomer("Gastro GmbH");
createB2cCustomer("Max Mustermann");
mockMvc.perform(get("/api/customers")
.param("type", "B2C")
.header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$[*].type", everyItem(is("B2C"))));
}
@Test
@DisplayName("TC-CUS-06: Nur aktive Kunden filtern → nur ACTIVE")
void listCustomers_filterByActive_returnsOnlyActive() throws Exception {
String id = createB2cCustomer("Inaktiver Kunde");
mockMvc.perform(post("/api/customers/{id}/deactivate", id)
.header("Authorization", "Bearer " + adminToken)).andReturn();
createB2cCustomer("Aktiver Kunde");
mockMvc.perform(get("/api/customers")
.param("status", "ACTIVE")
.header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$[*].status", everyItem(is("ACTIVE"))));
} }
// ==================== TC-CUS-07: Lieferadresse hinzufügen ==================== // ==================== TC-CUS-07: Lieferadresse hinzufügen ====================

View file

@ -122,7 +122,7 @@ class ProductCategoryControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/categories") mockMvc.perform(get("/api/categories")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$[?(@.id == '" + categoryId + "')]").isEmpty()); .andExpect(jsonPath("$.content[?(@.id == '" + categoryId + "')]").isEmpty());
} }
// ==================== TC-CAT-06: Leerer Name wird abgelehnt ==================== // ==================== TC-CAT-06: Leerer Name wird abgelehnt ====================
@ -160,7 +160,7 @@ class ProductCategoryControllerIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/categories") mockMvc.perform(get("/api/categories")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(greaterThanOrEqualTo(2)))); .andExpect(jsonPath("$.content", hasSize(greaterThanOrEqualTo(2))));
} }
// ==================== TC-AUTH: Autorisierung ==================== // ==================== TC-AUTH: Autorisierung ====================

View file

@ -173,7 +173,7 @@ class SupplierControllerIntegrationTest extends AbstractIntegrationTest {
.param("status", "ACTIVE") .param("status", "ACTIVE")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$[*].status", everyItem(is("ACTIVE")))); .andExpect(jsonPath("$.content[*].status", everyItem(is("ACTIVE"))));
} }
@Test @Test
@ -187,7 +187,7 @@ class SupplierControllerIntegrationTest extends AbstractIntegrationTest {
.param("status", "INACTIVE") .param("status", "INACTIVE")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$[*].status", everyItem(is("INACTIVE")))); .andExpect(jsonPath("$.content[*].status", everyItem(is("INACTIVE"))));
} }
// ==================== TC-SUP-07: Lieferant bewerten ==================== // ==================== TC-SUP-07: Lieferant bewerten ====================

View file

@ -42,10 +42,10 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/production/batches") mockMvc.perform(get("/api/production/batches")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2))) .andExpect(jsonPath("$.content", hasSize(2)))
.andExpect(jsonPath("$[0].id").isNotEmpty()) .andExpect(jsonPath("$.content[0].id").isNotEmpty())
.andExpect(jsonPath("$[0].batchNumber").isNotEmpty()) .andExpect(jsonPath("$.content[0].batchNumber").isNotEmpty())
.andExpect(jsonPath("$[0].status").value("PLANNED")); .andExpect(jsonPath("$.content[0].status").value("PLANNED"));
} }
@Test @Test
@ -54,7 +54,7 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/production/batches") mockMvc.perform(get("/api/production/batches")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(0))); .andExpect(jsonPath("$.content", hasSize(0)));
} }
@Test @Test
@ -66,8 +66,8 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest {
.param("status", "PLANNED") .param("status", "PLANNED")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$[0].status").value("PLANNED")); .andExpect(jsonPath("$.content[0].status").value("PLANNED"));
} }
@Test @Test
@ -90,8 +90,8 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest {
.param("productionDate", "2026-03-01") .param("productionDate", "2026-03-01")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$[0].productionDate").value("2026-03-01")); .andExpect(jsonPath("$.content[0].productionDate").value("2026-03-01"));
} }
@Test @Test
@ -103,7 +103,7 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest {
.param("articleId", "article-123") .param("articleId", "article-123")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1))); .andExpect(jsonPath("$.content", hasSize(1)));
} }
@Test @Test
@ -113,7 +113,7 @@ class ListBatchesIntegrationTest extends AbstractIntegrationTest {
.param("articleId", "unknown-article") .param("articleId", "unknown-article")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(0))); .andExpect(jsonPath("$.content", hasSize(0)));
} }
@Test @Test

View file

@ -41,11 +41,11 @@ class ListRecipesIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/recipes") mockMvc.perform(get("/api/recipes")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2))) .andExpect(jsonPath("$.content", hasSize(2)))
.andExpect(jsonPath("$[0].ingredientCount").isNumber()) .andExpect(jsonPath("$.content[0].ingredientCount").isNumber())
.andExpect(jsonPath("$[0].stepCount").isNumber()) .andExpect(jsonPath("$.content[0].stepCount").isNumber())
.andExpect(jsonPath("$[0].ingredients").doesNotExist()) .andExpect(jsonPath("$.content[0].ingredients").doesNotExist())
.andExpect(jsonPath("$[0].productionSteps").doesNotExist()); .andExpect(jsonPath("$.content[0].productionSteps").doesNotExist());
} }
@Test @Test
@ -62,9 +62,9 @@ class ListRecipesIntegrationTest extends AbstractIntegrationTest {
.param("status", "ACTIVE") .param("status", "ACTIVE")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1))) .andExpect(jsonPath("$.content", hasSize(1)))
.andExpect(jsonPath("$[0].name").value("Bratwurst")) .andExpect(jsonPath("$.content[0].name").value("Bratwurst"))
.andExpect(jsonPath("$[0].status").value("ACTIVE")); .andExpect(jsonPath("$.content[0].status").value("ACTIVE"));
} }
@Test @Test
@ -83,7 +83,7 @@ class ListRecipesIntegrationTest extends AbstractIntegrationTest {
mockMvc.perform(get("/api/recipes") mockMvc.perform(get("/api/recipes")
.header("Authorization", "Bearer " + adminToken)) .header("Authorization", "Bearer " + adminToken))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(0))); .andExpect(jsonPath("$.content", hasSize(0)));
} }
@Test @Test

Some files were not shown because too many files have changed in this diff Show more