mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 15:59:35 +01:00
feat(production): Rückwärts-Tracing für Herkunftsnachweis (US-P19)
Spiegelt die bestehende traceForward-Architektur mit invertierter SQL-JOIN-Richtung, um von einer Endprodukt-Charge alle verwendeten Rohstoff-Chargen zu ermitteln (Herkunftsnachweis).
This commit is contained in:
parent
252f48d52b
commit
6996a301f9
15 changed files with 632 additions and 6 deletions
|
|
@ -0,0 +1,28 @@
|
|||
package de.effigenix.application.production;
|
||||
|
||||
import de.effigenix.application.production.command.TraceBatchBackwardCommand;
|
||||
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 TraceBatchBackward {
|
||||
|
||||
private final BatchTraceabilityService traceabilityService;
|
||||
private final AuthorizationPort authorizationPort;
|
||||
|
||||
public TraceBatchBackward(BatchTraceabilityService traceabilityService, AuthorizationPort authorizationPort) {
|
||||
this.traceabilityService = traceabilityService;
|
||||
this.authorizationPort = authorizationPort;
|
||||
}
|
||||
|
||||
public Result<BatchError, List<TracedBatch>> execute(TraceBatchBackwardCommand command, ActorId performedBy) {
|
||||
if (!authorizationPort.can(performedBy, ProductionAction.BATCH_READ)) {
|
||||
return Result.failure(new BatchError.Unauthorized("Not authorized to read batches"));
|
||||
}
|
||||
|
||||
return traceabilityService.traceBackward(BatchId.of(command.batchId()));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package de.effigenix.application.production.command;
|
||||
|
||||
public record TraceBatchBackwardCommand(String batchId) {
|
||||
}
|
||||
|
|
@ -33,5 +33,7 @@ public interface BatchRepository {
|
|||
|
||||
Result<RepositoryError, List<TracedBatch>> traceForward(BatchId startBatchId, int maxDepth);
|
||||
|
||||
Result<RepositoryError, List<TracedBatch>> traceBackward(BatchId startBatchId, int maxDepth);
|
||||
|
||||
Result<RepositoryError, Void> save(Batch batch);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,4 +40,31 @@ public class BatchTraceabilityService {
|
|||
Result.success(traced);
|
||||
};
|
||||
}
|
||||
|
||||
public Result<BatchError, List<TracedBatch>> traceBackward(BatchId startBatchId) {
|
||||
return traceBackward(startBatchId, DEFAULT_MAX_DEPTH);
|
||||
}
|
||||
|
||||
public Result<BatchError, List<TracedBatch>> traceBackward(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maxDepth <= 0) {
|
||||
return Result.success(List.of());
|
||||
}
|
||||
|
||||
return switch (batchRepository.traceBackward(startBatchId, maxDepth)) {
|
||||
case Result.Failure(var err) ->
|
||||
Result.failure(new BatchError.RepositoryFailure(err.message()));
|
||||
case Result.Success(var traced) ->
|
||||
Result.success(traced);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.TraceBatchBackward;
|
||||
import de.effigenix.application.production.TraceBatchForward;
|
||||
import de.effigenix.application.production.FindBatchByNumber;
|
||||
import de.effigenix.application.production.GetBatch;
|
||||
|
|
@ -155,6 +156,12 @@ public class ProductionUseCaseConfiguration {
|
|||
return new TraceBatchForward(batchTraceabilityService, authorizationPort);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TraceBatchBackward traceBatchBackward(BatchTraceabilityService batchTraceabilityService,
|
||||
AuthorizationPort authorizationPort) {
|
||||
return new TraceBatchBackward(batchTraceabilityService, authorizationPort);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CreateProductionOrder createProductionOrder(ProductionOrderRepository productionOrderRepository,
|
||||
RecipeRepository recipeRepository,
|
||||
|
|
|
|||
|
|
@ -250,6 +250,44 @@ public class JdbcBatchRepository implements BatchRepository {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<TracedBatch>> traceBackward(BatchId startBatchId, int maxDepth) {
|
||||
try {
|
||||
String startId = startBatchId.value();
|
||||
var results = new ArrayList<TracedBatch>();
|
||||
var visited = new HashSet<String>();
|
||||
visited.add(startId);
|
||||
|
||||
var currentLevel = List.of(startId);
|
||||
for (int depth = 1; depth <= maxDepth && !currentLevel.isEmpty(); depth++) {
|
||||
int currentDepth = depth;
|
||||
var parents = jdbc.sql("""
|
||||
SELECT DISTINCT b.*
|
||||
FROM batches b
|
||||
JOIN batch_consumptions bc ON b.id = bc.input_batch_id
|
||||
WHERE bc.batch_id IN (:childIds)
|
||||
""")
|
||||
.param("childIds", currentLevel)
|
||||
.query(this::mapBatchRow)
|
||||
.list();
|
||||
|
||||
var nextLevel = new ArrayList<String>();
|
||||
for (Batch parent : parents) {
|
||||
if (visited.add(parent.id().value())) {
|
||||
results.add(new TracedBatch(parent, currentDepth));
|
||||
nextLevel.add(parent.id().value());
|
||||
}
|
||||
}
|
||||
currentLevel = nextLevel;
|
||||
}
|
||||
|
||||
return Result.success(results);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Database error in traceBackward", e);
|
||||
return Result.failure(new RepositoryError.DatabaseError(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, Void> save(Batch batch) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ 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.TraceBatchBackward;
|
||||
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.TraceBatchBackwardCommand;
|
||||
import de.effigenix.application.production.command.TraceBatchForwardCommand;
|
||||
import de.effigenix.domain.production.BatchError;
|
||||
import de.effigenix.domain.production.BatchId;
|
||||
|
|
@ -22,6 +24,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.TraceBatchBackwardResponse;
|
||||
import de.effigenix.infrastructure.production.web.dto.TraceBatchForwardResponse;
|
||||
import de.effigenix.infrastructure.production.web.dto.CancelBatchRequest;
|
||||
import de.effigenix.infrastructure.production.web.dto.CompleteBatchRequest;
|
||||
|
|
@ -60,11 +63,13 @@ public class BatchController {
|
|||
private final CompleteBatch completeBatch;
|
||||
private final CancelBatch cancelBatch;
|
||||
private final TraceBatchForward traceBatchForward;
|
||||
private final TraceBatchBackward traceBatchBackward;
|
||||
|
||||
public BatchController(PlanBatch planBatch, GetBatch getBatch, ListBatches listBatches,
|
||||
FindBatchByNumber findBatchByNumber, StartBatch startBatch,
|
||||
RecordConsumption recordConsumption, CompleteBatch completeBatch,
|
||||
CancelBatch cancelBatch, TraceBatchForward traceBatchForward) {
|
||||
CancelBatch cancelBatch, TraceBatchForward traceBatchForward,
|
||||
TraceBatchBackward traceBatchBackward) {
|
||||
this.planBatch = planBatch;
|
||||
this.getBatch = getBatch;
|
||||
this.listBatches = listBatches;
|
||||
|
|
@ -74,6 +79,7 @@ public class BatchController {
|
|||
this.completeBatch = completeBatch;
|
||||
this.cancelBatch = cancelBatch;
|
||||
this.traceBatchForward = traceBatchForward;
|
||||
this.traceBatchBackward = traceBatchBackward;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
|
|
@ -266,6 +272,23 @@ public class BatchController {
|
|||
return ResponseEntity.ok(TraceBatchForwardResponse.from(id, result.unsafeGetValue()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/trace-backward")
|
||||
@PreAuthorize("hasAuthority('BATCH_READ')")
|
||||
public ResponseEntity<TraceBatchBackwardResponse> traceBackward(
|
||||
@PathVariable("id") String id,
|
||||
Authentication authentication
|
||||
) {
|
||||
var actorId = ActorId.of(authentication.getName());
|
||||
var cmd = new TraceBatchBackwardCommand(id);
|
||||
var result = traceBatchBackward.execute(cmd, actorId);
|
||||
|
||||
if (result.isFailure()) {
|
||||
throw new BatchDomainErrorException(result.unsafeGetError());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(TraceBatchBackwardResponse.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 TraceBatchBackwardResponse(
|
||||
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 TraceBatchBackwardResponse from(String originBatchId, List<TracedBatch> tracedBatches) {
|
||||
var responses = tracedBatches.stream()
|
||||
.map(TracedBatchResponse::from)
|
||||
.toList();
|
||||
return new TraceBatchBackwardResponse(originBatchId, responses, responses.size());
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,11 @@ public class StubBatchRepository implements BatchRepository {
|
|||
return Result.failure(STUB_ERROR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<RepositoryError, List<TracedBatch>> traceBackward(BatchId startBatchId, int maxDepth) {
|
||||
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