From ca91a60cad58cd7a413234817e0089ea8ac612d5 Mon Sep 17 00:00:00 2001 From: Marcus van Elst Date: Fri, 3 Apr 2026 07:52:21 +0200 Subject: [PATCH] M4 AP-006 Reihenfolge, Konsistenz und Scope bereinigen --- .../service/M4DocumentProcessor.java | 96 ++++++++++++++++++- .../DefaultBatchRunProcessingUseCase.java | 32 ++++--- .../BatchRunProcessingUseCaseTest.java | 14 ++- 3 files changed, 127 insertions(+), 15 deletions(-) diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/M4DocumentProcessor.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/M4DocumentProcessor.java index e0546a4..0e0fab5 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/M4DocumentProcessor.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/service/M4DocumentProcessor.java @@ -192,6 +192,100 @@ public class M4DocumentProcessor { } } + /** + * Applies the full M4 processing logic for one identified document candidate. + *

+ * The caller must have already computed a valid {@link DocumentFingerprint} for the + * candidate. This method handles the complete M4 processing flow: + *

    + *
  1. Load document master record.
  2. + *
  3. Handle terminal SUCCESS / FAILED_FINAL skip cases first.
  4. + *
  5. Only if not terminal: execute the M3 flow (PDF extraction + pre-checks).
  6. + *
  7. Map M3 outcome to M4 status, counters and retryable flag.
  8. + *
  9. Persist exactly one historised processing attempt.
  10. + *
  11. Persist the updated document master record.
  12. + *
+ *

+ * This method never throws. All persistence failures are caught, logged, and + * treated as controlled per-document failures so the batch run can continue. + * + * @param candidate the source document candidate being processed; must not be null + * @param fingerprint the successfully computed fingerprint for this candidate; + * must not be null + * @param context the current batch run context (for run ID and timing); + * must not be null + * @param attemptStart the instant at which processing of this candidate began; + * must not be null + * @param m3Executor functional interface to execute the M3 pipeline when needed; + * must not be null + */ + public void processWithM3Execution( + SourceDocumentCandidate candidate, + DocumentFingerprint fingerprint, + BatchRunContext context, + Instant attemptStart, + java.util.function.Function m3Executor) { + + Objects.requireNonNull(candidate, "candidate must not be null"); + Objects.requireNonNull(fingerprint, "fingerprint must not be null"); + Objects.requireNonNull(context, "context must not be null"); + Objects.requireNonNull(attemptStart, "attemptStart must not be null"); + Objects.requireNonNull(m3Executor, "m3Executor must not be null"); + + // Step 1: Load the document master record + DocumentRecordLookupResult lookupResult = + documentRecordRepository.findByFingerprint(fingerprint); + + // Step 2: Handle persistence lookup failure – cannot safely proceed + if (lookupResult instanceof PersistenceLookupTechnicalFailure failure) { + LOG.error("Cannot process '{}': master record lookup failed: {}", + candidate.uniqueIdentifier(), failure.errorMessage()); + return; + } + + // Step 3: Determine the action based on the lookup result + switch (lookupResult) { + case DocumentTerminalSuccess terminalSuccess -> { + // Document already successfully processed → skip + LOG.info("Skipping '{}': already successfully processed (fingerprint: {}).", + candidate.uniqueIdentifier(), fingerprint.sha256Hex()); + persistSkipAttempt( + candidate, fingerprint, terminalSuccess.record(), + ProcessingStatus.SKIPPED_ALREADY_PROCESSED, + context, attemptStart); + } + + case DocumentTerminalFinalFailure terminalFailure -> { + // Document finally failed → skip + LOG.info("Skipping '{}': already finally failed (fingerprint: {}).", + candidate.uniqueIdentifier(), fingerprint.sha256Hex()); + persistSkipAttempt( + candidate, fingerprint, terminalFailure.record(), + ProcessingStatus.SKIPPED_FINAL_FAILURE, + context, attemptStart); + } + + case DocumentUnknown ignored -> { + // New document – execute M3 pipeline and process + DocumentProcessingOutcome m3Outcome = m3Executor.apply(candidate); + processAndPersistNewDocument(candidate, fingerprint, m3Outcome, context, attemptStart); + } + + case DocumentKnownProcessable knownProcessable -> { + // Known but not terminal – execute M3 pipeline and process + DocumentProcessingOutcome m3Outcome = m3Executor.apply(candidate); + processAndPersistKnownDocument( + candidate, fingerprint, m3Outcome, knownProcessable.record(), + context, attemptStart); + } + + default -> + // Exhaustive sealed hierarchy; this branch is unreachable + LOG.error("Unexpected lookup result type for '{}': {}", + candidate.uniqueIdentifier(), lookupResult.getClass().getSimpleName()); + } + } + // ------------------------------------------------------------------------- // Skip path // ------------------------------------------------------------------------- @@ -555,4 +649,4 @@ public class M4DocumentProcessor { FailureCounters counters, boolean retryable) { } -} +} \ No newline at end of file diff --git a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultBatchRunProcessingUseCase.java b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultBatchRunProcessingUseCase.java index 669c749..d8b4d7c 100644 --- a/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultBatchRunProcessingUseCase.java +++ b/pdf-umbenenner-application/src/main/java/de/gecheckt/pdf/umbenenner/application/usecase/DefaultBatchRunProcessingUseCase.java @@ -194,9 +194,15 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa *

  • Compute the SHA-256 fingerprint of the candidate file content.
  • *
  • If fingerprint computation fails: log as non-identifiable run event and * return — no SQLite record is created.
  • - *
  • Execute the M3 pipeline (PDF extraction + pre-checks).
  • - *
  • Delegate to {@link M4DocumentProcessor} for idempotency check, status/counter - * mapping, and consistent two-level persistence.
  • + *
  • Load document master record.
  • + *
  • If already {@code SUCCESS} → persist skip attempt with + * {@code SKIPPED_ALREADY_PROCESSED}.
  • + *
  • If already {@code FAILED_FINAL} → persist skip attempt with + * {@code SKIPPED_FINAL_FAILURE}.
  • + *
  • Otherwise execute the M3 pipeline (extraction + pre-checks).
  • + *
  • Map M3 result into M4 status, counters and retryable flag.
  • + *
  • Persist exactly one historised processing attempt.
  • + *
  • Persist the updated document master record.
  • * *

    * Per-document errors do not abort the overall batch run. Each candidate ends @@ -227,15 +233,15 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa LOG.debug("Fingerprint computed for '{}': {}", candidate.uniqueIdentifier(), fingerprint.sha256Hex()); - // Step M4-2..M4-8: Execute M3 pipeline and delegate M4 logic to the processor - // The M3 pipeline runs only if the document is not in a terminal state; - // M4DocumentProcessor handles the terminal check internally. - // We run M3 eagerly here and pass the result; M4DocumentProcessor will - // ignore it for terminal documents. - DocumentProcessingOutcome m3Outcome = runM3Pipeline(candidate); - - // Delegate idempotency check, status mapping, and persistence to M4DocumentProcessor - m4DocumentProcessor.process(candidate, fingerprint, m3Outcome, context, attemptStart); + // Delegate the complete M4 processing logic to the processor + // The processor handles loading document master record, checking terminal status, + // executing M3 pipeline only when needed, and persisting results consistently + m4DocumentProcessor.processWithM3Execution( + candidate, + fingerprint, + context, + attemptStart, + this::runM3Pipeline); // Pass the M3 executor as a function } } } @@ -287,4 +293,4 @@ public class DefaultBatchRunProcessingUseCase implements BatchRunProcessingUseCa return outcome; } -} +} \ No newline at end of file diff --git a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java index f13411f..5fbc692 100644 --- a/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java +++ b/pdf-umbenenner-application/src/test/java/de/gecheckt/pdf/umbenenner/application/usecase/BatchRunProcessingUseCaseTest.java @@ -641,6 +641,18 @@ class BatchRunProcessingUseCaseTest { super.process(candidate, fingerprint, m3Outcome, context, attemptStart); } + @Override + public void processWithM3Execution( + de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentCandidate candidate, + de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint fingerprint, + de.gecheckt.pdf.umbenenner.domain.model.BatchRunContext context, + java.time.Instant attemptStart, + java.util.function.Function m3Executor) { + processCallCount++; + // Delegate to super so the real logic runs (with no-op repos) + super.processWithM3Execution(candidate, fingerprint, context, attemptStart, m3Executor); + } + int processCallCount() { return processCallCount; } } @@ -680,4 +692,4 @@ class BatchRunProcessingUseCaseTest { return List.of(); } } -} +} \ No newline at end of file