1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 08:29:36 +01:00

feat(loadtest): Gatling-Lasttests mit ~2500 Requests für komprimiertes Jahres-Volumen

Szenarien: Stammdaten-CRUD, Produktions-Workflow, Lagerverwaltung,
Read-Only-Zugriffe. Batch-Repository auf Summary-Projektion umgestellt,
Permissions-Changeset Merge-Konflikt aufgelöst, Unit-Enum im
JsonBodyBuilder korrigiert (KILOGRAM → KG).
This commit is contained in:
Sebastian Frick 2026-02-24 21:44:16 +01:00
parent 8a9bf849a9
commit 11fb62383b
21 changed files with 1856 additions and 38 deletions

View file

@ -128,6 +128,7 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>

View file

@ -28,7 +28,7 @@ public class ListBatches {
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
}
switch (batchRepository.findAll()) {
switch (batchRepository.findAllSummary()) {
case Result.Failure(var err) ->
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); }
case Result.Success(var batches) ->
@ -41,7 +41,7 @@ public class ListBatches {
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
}
switch (batchRepository.findByStatus(status)) {
switch (batchRepository.findByStatusSummary(status)) {
case Result.Failure(var err) ->
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); }
case Result.Success(var batches) ->
@ -54,7 +54,7 @@ public class ListBatches {
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
}
switch (batchRepository.findByProductionDate(date)) {
switch (batchRepository.findByProductionDateSummary(date)) {
case Result.Failure(var err) ->
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); }
case Result.Success(var batches) ->
@ -75,7 +75,7 @@ public class ListBatches {
return Result.success(List.of());
}
List<RecipeId> recipeIds = recipes.stream().map(Recipe::id).toList();
switch (batchRepository.findByRecipeIds(recipeIds)) {
switch (batchRepository.findByRecipeIdsSummary(recipeIds)) {
case Result.Failure(var batchErr) ->
{ return Result.failure(new BatchError.RepositoryFailure(batchErr.message())); }
case Result.Success(var batches) ->

View file

@ -21,5 +21,13 @@ public interface BatchRepository {
Result<RepositoryError, List<Batch>> findByRecipeIds(List<RecipeId> recipeIds);
Result<RepositoryError, List<Batch>> findAllSummary();
Result<RepositoryError, List<Batch>> findByStatusSummary(BatchStatus status);
Result<RepositoryError, List<Batch>> findByProductionDateSummary(LocalDate date);
Result<RepositoryError, List<Batch>> findByRecipeIdsSummary(List<RecipeId> recipeIds);
Result<RepositoryError, Void> save(Batch batch);
}

View file

@ -3,11 +3,14 @@ package de.effigenix.infrastructure.masterdata.persistence.repository;
import de.effigenix.infrastructure.masterdata.persistence.entity.FrameContractEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface FrameContractJpaRepository extends JpaRepository<FrameContractEntity, String> {
Optional<FrameContractEntity> findByCustomerId(String customerId);
List<FrameContractEntity> findByCustomerIdIn(List<String> customerIds);
void deleteByCustomerId(String customerId);
}

View file

@ -1,6 +1,7 @@
package de.effigenix.infrastructure.masterdata.persistence.repository;
import de.effigenix.domain.masterdata.*;
import de.effigenix.infrastructure.masterdata.persistence.entity.CustomerEntity;
import de.effigenix.infrastructure.masterdata.persistence.entity.FrameContractEntity;
import de.effigenix.infrastructure.masterdata.persistence.mapper.CustomerMapper;
import de.effigenix.shared.common.RepositoryError;
@ -12,7 +13,9 @@ import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
@Repository
@ -52,13 +55,7 @@ public class JpaCustomerRepository implements CustomerRepository {
@Override
public Result<RepositoryError, List<Customer>> findAll() {
try {
List<Customer> result = jpaRepository.findAll().stream()
.map(entity -> {
var fc = frameContractJpaRepository.findByCustomerId(entity.getId()).orElse(null);
return mapper.toDomain(entity, fc);
})
.collect(Collectors.toList());
return Result.success(result);
return Result.success(mapWithFrameContracts(jpaRepository.findAll()));
} catch (Exception e) {
logger.trace("Database error in findAll", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
@ -68,13 +65,7 @@ public class JpaCustomerRepository implements CustomerRepository {
@Override
public Result<RepositoryError, List<Customer>> findByType(CustomerType type) {
try {
List<Customer> result = jpaRepository.findByType(type.name()).stream()
.map(entity -> {
var fc = frameContractJpaRepository.findByCustomerId(entity.getId()).orElse(null);
return mapper.toDomain(entity, fc);
})
.collect(Collectors.toList());
return Result.success(result);
return Result.success(mapWithFrameContracts(jpaRepository.findByType(type.name())));
} catch (Exception e) {
logger.trace("Database error in findByType", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
@ -84,19 +75,24 @@ public class JpaCustomerRepository implements CustomerRepository {
@Override
public Result<RepositoryError, List<Customer>> findByStatus(CustomerStatus status) {
try {
List<Customer> result = jpaRepository.findByStatus(status.name()).stream()
.map(entity -> {
var fc = frameContractJpaRepository.findByCustomerId(entity.getId()).orElse(null);
return mapper.toDomain(entity, fc);
})
.collect(Collectors.toList());
return Result.success(result);
return Result.success(mapWithFrameContracts(jpaRepository.findByStatus(status.name())));
} catch (Exception e) {
logger.trace("Database error in findByStatus", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
private List<Customer> mapWithFrameContracts(List<CustomerEntity> entities) {
List<String> customerIds = entities.stream()
.map(CustomerEntity::getId)
.toList();
Map<String, FrameContractEntity> fcMap = frameContractJpaRepository.findByCustomerIdIn(customerIds).stream()
.collect(Collectors.toMap(FrameContractEntity::getCustomerId, Function.identity()));
return entities.stream()
.map(entity -> mapper.toDomain(entity, fcMap.get(entity.getId())))
.collect(Collectors.toList());
}
@Override
@Transactional
public Result<RepositoryError, Void> save(Customer customer) {

View file

@ -128,6 +128,38 @@ public class BatchMapper {
);
}
public Batch toDomainSummary(BatchEntity entity) {
Quantity actualQuantity = entity.getActualQuantityAmount() != null
? Quantity.reconstitute(entity.getActualQuantityAmount(), UnitOfMeasure.valueOf(entity.getActualQuantityUnit()))
: null;
Quantity waste = entity.getWasteAmount() != null
? Quantity.reconstitute(entity.getWasteAmount(), UnitOfMeasure.valueOf(entity.getWasteUnit()))
: null;
return Batch.reconstitute(
BatchId.of(entity.getId()),
new BatchNumber(entity.getBatchNumber()),
RecipeId.of(entity.getRecipeId()),
BatchStatus.valueOf(entity.getStatus()),
Quantity.reconstitute(
entity.getPlannedQuantityAmount(),
UnitOfMeasure.valueOf(entity.getPlannedQuantityUnit())
),
actualQuantity,
waste,
entity.getRemarks(),
entity.getProductionDate(),
entity.getBestBeforeDate(),
entity.getCreatedAt(),
entity.getUpdatedAt(),
entity.getCompletedAt(),
entity.getCancellationReason(),
entity.getCancelledAt(),
entity.getVersion(),
List.of()
);
}
private ConsumptionEntity toConsumptionEntity(Consumption c, BatchEntity parent) {
return new ConsumptionEntity(
c.id().value(),

View file

@ -108,6 +108,59 @@ public class JpaBatchRepository implements BatchRepository {
}
}
@Override
public Result<RepositoryError, List<Batch>> findAllSummary() {
try {
List<Batch> result = jpaRepository.findAll().stream()
.map(mapper::toDomainSummary)
.collect(Collectors.toList());
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findAllSummary", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Batch>> findByStatusSummary(BatchStatus status) {
try {
List<Batch> result = jpaRepository.findByStatus(status.name()).stream()
.map(mapper::toDomainSummary)
.collect(Collectors.toList());
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findByStatusSummary", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Batch>> findByProductionDateSummary(LocalDate date) {
try {
List<Batch> result = jpaRepository.findByProductionDate(date).stream()
.map(mapper::toDomainSummary)
.collect(Collectors.toList());
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findByProductionDateSummary", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
public Result<RepositoryError, List<Batch>> findByRecipeIdsSummary(List<RecipeId> recipeIds) {
try {
List<String> ids = recipeIds.stream().map(RecipeId::value).toList();
List<Batch> result = jpaRepository.findByRecipeIdIn(ids).stream()
.map(mapper::toDomainSummary)
.collect(Collectors.toList());
return Result.success(result);
} catch (Exception e) {
logger.trace("Database error in findByRecipeIdsSummary", e);
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
}
}
@Override
@Transactional
public Result<RepositoryError, Void> save(Batch batch) {

View file

@ -6,7 +6,16 @@
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="025-seed-production-order-permissions" author="effigenix">
<comment>Add PRODUCTION_ORDER_READ and PRODUCTION_ORDER_WRITE permissions for ADMIN and PRODUCTION_MANAGER roles (idempotent)</comment>
<preConditions onFail="MARK_RAN">
<not>
<sqlCheck expectedResult="1">
SELECT COUNT(*) FROM role_permissions
WHERE role_id = 'c0a80121-0000-0000-0000-000000000001'
AND permission = 'PRODUCTION_ORDER_READ'
</sqlCheck>
</not>
</preConditions>
<comment>Add PRODUCTION_ORDER_READ and PRODUCTION_ORDER_WRITE permissions for ADMIN and PRODUCTION_MANAGER roles (skipped if already present from 002)</comment>
<sql>
INSERT INTO role_permissions (role_id, permission) VALUES

View file

@ -81,7 +81,7 @@ class ListBatchesTest {
void should_ReturnAllBatches() {
var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED), sampleBatch("b2", BatchStatus.PLANNED));
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findAll()).thenReturn(Result.success(batches));
when(batchRepository.findAllSummary()).thenReturn(Result.success(batches));
var result = listBatches.execute(performedBy);
@ -93,7 +93,7 @@ class ListBatchesTest {
@DisplayName("should return empty list when no batches exist")
void should_ReturnEmptyList_When_NoBatchesExist() {
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findAll()).thenReturn(Result.success(List.of()));
when(batchRepository.findAllSummary()).thenReturn(Result.success(List.of()));
var result = listBatches.execute(performedBy);
@ -105,7 +105,7 @@ class ListBatchesTest {
@DisplayName("should fail with RepositoryFailure when findAll fails")
void should_FailWithRepositoryFailure_When_FindAllFails() {
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findAll()).thenReturn(Result.failure(DB_ERROR));
when(batchRepository.findAllSummary()).thenReturn(Result.failure(DB_ERROR));
var result = listBatches.execute(performedBy);
@ -135,7 +135,7 @@ class ListBatchesTest {
void should_ReturnBatches_FilteredByStatus() {
var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED));
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findByStatus(BatchStatus.PLANNED)).thenReturn(Result.success(batches));
when(batchRepository.findByStatusSummary(BatchStatus.PLANNED)).thenReturn(Result.success(batches));
var result = listBatches.executeByStatus(BatchStatus.PLANNED, performedBy);
@ -147,7 +147,7 @@ class ListBatchesTest {
@DisplayName("should return empty list when no batches match status")
void should_ReturnEmptyList_When_NoBatchesMatchStatus() {
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findByStatus(BatchStatus.PLANNED)).thenReturn(Result.success(List.of()));
when(batchRepository.findByStatusSummary(BatchStatus.PLANNED)).thenReturn(Result.success(List.of()));
var result = listBatches.executeByStatus(BatchStatus.PLANNED, performedBy);
@ -159,7 +159,7 @@ class ListBatchesTest {
@DisplayName("should fail with RepositoryFailure when findByStatus fails")
void should_FailWithRepositoryFailure_When_FindByStatusFails() {
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findByStatus(BatchStatus.PLANNED)).thenReturn(Result.failure(DB_ERROR));
when(batchRepository.findByStatusSummary(BatchStatus.PLANNED)).thenReturn(Result.failure(DB_ERROR));
var result = listBatches.executeByStatus(BatchStatus.PLANNED, performedBy);
@ -189,7 +189,7 @@ class ListBatchesTest {
void should_ReturnBatches_FilteredByProductionDate() {
var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED));
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findByProductionDate(PRODUCTION_DATE)).thenReturn(Result.success(batches));
when(batchRepository.findByProductionDateSummary(PRODUCTION_DATE)).thenReturn(Result.success(batches));
var result = listBatches.executeByProductionDate(PRODUCTION_DATE, performedBy);
@ -201,7 +201,7 @@ class ListBatchesTest {
@DisplayName("should return empty list when no batches match date")
void should_ReturnEmptyList_When_NoBatchesMatchDate() {
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findByProductionDate(PRODUCTION_DATE)).thenReturn(Result.success(List.of()));
when(batchRepository.findByProductionDateSummary(PRODUCTION_DATE)).thenReturn(Result.success(List.of()));
var result = listBatches.executeByProductionDate(PRODUCTION_DATE, performedBy);
@ -213,7 +213,7 @@ class ListBatchesTest {
@DisplayName("should fail with RepositoryFailure when findByProductionDate fails")
void should_FailWithRepositoryFailure_When_FindByProductionDateFails() {
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(batchRepository.findByProductionDate(PRODUCTION_DATE)).thenReturn(Result.failure(DB_ERROR));
when(batchRepository.findByProductionDateSummary(PRODUCTION_DATE)).thenReturn(Result.failure(DB_ERROR));
var result = listBatches.executeByProductionDate(PRODUCTION_DATE, performedBy);
@ -245,7 +245,7 @@ class ListBatchesTest {
var batches = List.of(sampleBatch("b1", BatchStatus.PLANNED));
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(recipeRepository.findByArticleId("article-123")).thenReturn(Result.success(List.of(recipe)));
when(batchRepository.findByRecipeIds(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.success(batches));
when(batchRepository.findByRecipeIdsSummary(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.success(batches));
var result = listBatches.executeByArticleId("article-123", performedBy);
@ -272,7 +272,7 @@ class ListBatchesTest {
var recipe = sampleRecipe("recipe-1");
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(recipeRepository.findByArticleId("article-123")).thenReturn(Result.success(List.of(recipe)));
when(batchRepository.findByRecipeIds(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.success(List.of()));
when(batchRepository.findByRecipeIdsSummary(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.success(List.of()));
var result = listBatches.executeByArticleId("article-123", performedBy);
@ -299,7 +299,7 @@ class ListBatchesTest {
var recipe = sampleRecipe("recipe-1");
when(authPort.can(performedBy, ProductionAction.BATCH_READ)).thenReturn(true);
when(recipeRepository.findByArticleId("article-123")).thenReturn(Result.success(List.of(recipe)));
when(batchRepository.findByRecipeIds(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.failure(DB_ERROR));
when(batchRepository.findByRecipeIdsSummary(List.of(RecipeId.of("recipe-1")))).thenReturn(Result.failure(DB_ERROR));
var result = listBatches.executeByArticleId("article-123", performedBy);