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:
parent
8a9bf849a9
commit
11fb62383b
21 changed files with 1856 additions and 38 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) ->
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue