mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 17:49:57 +01:00
feat(production): Vorwärts-Tracing für Rückruf-Szenario (US-P18)
BFS-Traversierung über Chargen-Genealogie (batch_consumptions.input_batch_id)
mit Cycle-Detection und Max-Depth-Guard. REST-Endpoint GET /{id}/trace-forward
liefert flache Liste mit Tiefenangabe für betroffene Endprodukt-Chargen.
This commit is contained in:
parent
973c33d78f
commit
ddb674d618
14 changed files with 822 additions and 1 deletions
|
|
@ -0,0 +1,28 @@
|
|||
package de.effigenix.application.production;
|
||||
|
||||
import de.effigenix.application.production.command.TraceBatchForwardCommand;
|
||||
import de.effigenix.domain.production.*;
|
||||
import de.effigenix.shared.common.Result;
|
||||
import de.effigenix.shared.security.ActorId;
|
||||
import de.effigenix.shared.security.AuthorizationPort;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TraceBatchForward {
|
||||
|
||||
private final BatchTraceabilityService traceabilityService;
|
||||
private final AuthorizationPort authorizationPort;
|
||||
|
||||
public TraceBatchForward(BatchTraceabilityService traceabilityService, AuthorizationPort authorizationPort) {
|
||||
this.traceabilityService = traceabilityService;
|
||||
this.authorizationPort = authorizationPort;
|
||||
}
|
||||
|
||||
public Result<BatchError, List<TracedBatch>> execute(TraceBatchForwardCommand command, ActorId performedBy) {
|
||||
if (!authorizationPort.can(performedBy, ProductionAction.BATCH_READ)) {
|
||||
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
|
||||
}
|
||||
|
||||
return traceabilityService.traceForward(BatchId.of(command.batchId()));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package de.effigenix.application.production.command;
|
||||
|
||||
public record TraceBatchForwardCommand(String batchId) {
|
||||
}
|
||||
|
|
@ -29,5 +29,7 @@ public interface BatchRepository {
|
|||
|
||||
Result<RepositoryError, List<Batch>> findByRecipeIdsSummary(List<RecipeId> recipeIds);
|
||||
|
||||
Result<RepositoryError, List<Batch>> findByInputBatchId(BatchId inputBatchId);
|
||||
|
||||
Result<RepositoryError, Void> save(Batch batch);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package de.effigenix.domain.production;
|
||||
|
||||
import de.effigenix.shared.common.Result;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class BatchTraceabilityService {
|
||||
|
||||
static final int DEFAULT_MAX_DEPTH = 10;
|
||||
|
||||
private final BatchRepository batchRepository;
|
||||
|
||||
public BatchTraceabilityService(BatchRepository batchRepository) {
|
||||
this.batchRepository = batchRepository;
|
||||
}
|
||||
|
||||
public Result<BatchError, List<TracedBatch>> traceForward(BatchId startBatchId) {
|
||||
return traceForward(startBatchId, DEFAULT_MAX_DEPTH);
|
||||
}
|
||||
|
||||
public Result<BatchError, List<TracedBatch>> traceForward(BatchId startBatchId, int maxDepth) {
|
||||
switch (batchRepository.findById(startBatchId)) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var opt) -> {
|
||||
if (opt.isEmpty()) {
|
||||
return Result.failure(new BatchError.BatchNotFound(startBatchId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<TracedBatch> result = new ArrayList<>();
|
||||
Set<String> visited = new HashSet<>();
|
||||
visited.add(startBatchId.value());
|
||||
|
||||
record BfsEntry(BatchId batchId, int depth) {}
|
||||
var queue = new ArrayDeque<BfsEntry>();
|
||||
queue.add(new BfsEntry(startBatchId, 0));
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
var entry = queue.poll();
|
||||
if (entry.depth() >= maxDepth) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (batchRepository.findByInputBatchId(entry.batchId())) {
|
||||
case Result.Failure(var err) ->
|
||||
{ return Result.failure(new BatchError.RepositoryFailure(err.message())); }
|
||||
case Result.Success(var children) -> {
|
||||
int childDepth = entry.depth() + 1;
|
||||
for (Batch child : children) {
|
||||
if (visited.add(child.id().value())) {
|
||||
result.add(new TracedBatch(child, childDepth));
|
||||
queue.add(new BfsEntry(child.id(), childDepth));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(result);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package de.effigenix.domain.production;
|
||||
|
||||
public record TracedBatch(Batch batch, int depth) {
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ import de.effigenix.application.production.StartProductionOrder;
|
|||
import de.effigenix.application.production.CancelBatch;
|
||||
import de.effigenix.application.production.CompleteBatch;
|
||||
import de.effigenix.application.production.CreateRecipe;
|
||||
import de.effigenix.application.production.TraceBatchForward;
|
||||
import de.effigenix.application.production.FindBatchByNumber;
|
||||
import de.effigenix.application.production.GetBatch;
|
||||
import de.effigenix.application.production.ListBatches;
|
||||
|
|
@ -28,6 +29,7 @@ import de.effigenix.application.production.RemoveProductionStep;
|
|||
import de.effigenix.application.production.RemoveRecipeIngredient;
|
||||
import de.effigenix.domain.production.BatchNumberGenerator;
|
||||
import de.effigenix.domain.production.BatchRepository;
|
||||
import de.effigenix.domain.production.BatchTraceabilityService;
|
||||
import de.effigenix.domain.production.ProductionOrderRepository;
|
||||
import de.effigenix.domain.production.RecipeRepository;
|
||||
import de.effigenix.shared.persistence.UnitOfWork;
|
||||
|
|
@ -142,6 +144,17 @@ public class ProductionUseCaseConfiguration {
|
|||
return new CancelBatch(batchRepository, authorizationPort, unitOfWork);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BatchTraceabilityService batchTraceabilityService(BatchRepository batchRepository) {
|
||||
return new BatchTraceabilityService(batchRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TraceBatchForward traceBatchForward(BatchTraceabilityService batchTraceabilityService,
|
||||
AuthorizationPort authorizationPort) {
|
||||
return new TraceBatchForward(batchTraceabilityService, authorizationPort);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CreateProductionOrder createProductionOrder(ProductionOrderRepository productionOrderRepository,
|
||||
RecipeRepository recipeRepository,
|
||||
|
|
|
|||
|
|
@ -192,6 +192,24 @@ public class JdbcBatchRepository implements BatchRepository {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<Batch>> findByInputBatchId(BatchId inputBatchId) {
|
||||
try {
|
||||
var batches = jdbc.sql("""
|
||||
SELECT b.* FROM batches b
|
||||
JOIN batch_consumptions bc ON b.id = bc.batch_id
|
||||
WHERE bc.input_batch_id = :inputBatchId
|
||||
""")
|
||||
.param("inputBatchId", inputBatchId.value())
|
||||
.query(this::mapBatchRow)
|
||||
.list();
|
||||
return Result.success(batches);
|
||||
} catch (Exception e) {
|
||||
logger.trace("Database error in findByInputBatchId", e);
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, Void> save(Batch batch) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ import de.effigenix.application.production.ListBatches;
|
|||
import de.effigenix.application.production.PlanBatch;
|
||||
import de.effigenix.application.production.RecordConsumption;
|
||||
import de.effigenix.application.production.StartBatch;
|
||||
import de.effigenix.application.production.TraceBatchForward;
|
||||
import de.effigenix.application.production.command.CancelBatchCommand;
|
||||
import de.effigenix.application.production.command.CompleteBatchCommand;
|
||||
import de.effigenix.application.production.command.PlanBatchCommand;
|
||||
import de.effigenix.application.production.command.RecordConsumptionCommand;
|
||||
import de.effigenix.application.production.command.StartBatchCommand;
|
||||
import de.effigenix.application.production.command.TraceBatchForwardCommand;
|
||||
import de.effigenix.domain.production.BatchError;
|
||||
import de.effigenix.domain.production.BatchId;
|
||||
import de.effigenix.domain.production.BatchNumber;
|
||||
|
|
@ -20,6 +22,7 @@ import de.effigenix.domain.production.BatchStatus;
|
|||
import de.effigenix.infrastructure.production.web.dto.BatchResponse;
|
||||
import de.effigenix.infrastructure.production.web.dto.BatchSummaryResponse;
|
||||
import de.effigenix.infrastructure.production.web.dto.ConsumptionResponse;
|
||||
import de.effigenix.infrastructure.production.web.dto.TraceBatchForwardResponse;
|
||||
import de.effigenix.infrastructure.production.web.dto.CancelBatchRequest;
|
||||
import de.effigenix.infrastructure.production.web.dto.CompleteBatchRequest;
|
||||
import de.effigenix.infrastructure.production.web.dto.PlanBatchRequest;
|
||||
|
|
@ -56,11 +59,12 @@ public class BatchController {
|
|||
private final RecordConsumption recordConsumption;
|
||||
private final CompleteBatch completeBatch;
|
||||
private final CancelBatch cancelBatch;
|
||||
private final TraceBatchForward traceBatchForward;
|
||||
|
||||
public BatchController(PlanBatch planBatch, GetBatch getBatch, ListBatches listBatches,
|
||||
FindBatchByNumber findBatchByNumber, StartBatch startBatch,
|
||||
RecordConsumption recordConsumption, CompleteBatch completeBatch,
|
||||
CancelBatch cancelBatch) {
|
||||
CancelBatch cancelBatch, TraceBatchForward traceBatchForward) {
|
||||
this.planBatch = planBatch;
|
||||
this.getBatch = getBatch;
|
||||
this.listBatches = listBatches;
|
||||
|
|
@ -69,6 +73,7 @@ public class BatchController {
|
|||
this.recordConsumption = recordConsumption;
|
||||
this.completeBatch = completeBatch;
|
||||
this.cancelBatch = cancelBatch;
|
||||
this.traceBatchForward = traceBatchForward;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
|
|
@ -244,6 +249,23 @@ public class BatchController {
|
|||
return ResponseEntity.ok(BatchResponse.from(result.unsafeGetValue()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/trace-forward")
|
||||
@PreAuthorize("hasAuthority('BATCH_READ')")
|
||||
public ResponseEntity<TraceBatchForwardResponse> traceForward(
|
||||
@PathVariable("id") String id,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = ActorId.of(authentication.getName());
|
||||
var cmd = new TraceBatchForwardCommand(id);
|
||||
var result = traceBatchForward.execute(cmd, actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new BatchDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(TraceBatchForwardResponse.from(id, result.unsafeGetValue()));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/cancel")
|
||||
@PreAuthorize("hasAuthority('BATCH_CANCEL')")
|
||||
public ResponseEntity<BatchResponse> cancelBatch(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package de.effigenix.infrastructure.production.web.dto;
|
||||
|
||||
import de.effigenix.domain.production.TracedBatch;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record TraceBatchForwardResponse(
|
||||
String originBatchId,
|
||||
List<TracedBatchResponse> tracedBatches,
|
||||
int totalCount
|
||||
) {
|
||||
public record TracedBatchResponse(
|
||||
String id,
|
||||
String batchNumber,
|
||||
String recipeId,
|
||||
String status,
|
||||
int depth
|
||||
) {
|
||||
public static TracedBatchResponse from(TracedBatch traced) {
|
||||
var batch = traced.batch();
|
||||
return new TracedBatchResponse(
|
||||
batch.id().value(),
|
||||
batch.batchNumber().value(),
|
||||
batch.recipeId().value(),
|
||||
batch.status().name(),
|
||||
traced.depth()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static TraceBatchForwardResponse from(String originBatchId, List<TracedBatch> tracedBatches) {
|
||||
var responses = tracedBatches.stream()
|
||||
.map(TracedBatchResponse::from)
|
||||
.toList();
|
||||
return new TraceBatchForwardResponse(originBatchId, responses, responses.size());
|
||||
}
|
||||
}
|
||||
|
|
@ -72,6 +72,11 @@ public class StubBatchRepository implements BatchRepository {
|
|||
return Result.failure(STUB_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<Batch>> findByInputBatchId(BatchId inputBatchId) {
|
||||
return Result.failure(STUB_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, Void> save(Batch batch) {
|
||||
return Result.failure(STUB_ERROR);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue