diff --git a/DocumentOperator.API/Controllers/PdfValidationController.cs b/DocumentOperator.API/Controllers/PdfValidationController.cs
new file mode 100644
index 0000000..1b4a6fd
--- /dev/null
+++ b/DocumentOperator.API/Controllers/PdfValidationController.cs
@@ -0,0 +1,79 @@
+using DocumentOperator.Application.Common.DTOs;
+using DocumentOperator.Application.ValidatePdf.Queries;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace DocumentOperator.API.Controllers;
+
+///
+/// PDF validation operations
+///
+[ApiController]
+[Route("api/pdf/validation")]
+[Produces("application/json")]
+public class PdfValidationController(IMediator Mediator) : ControllerBase
+{
+ ///
+ /// Validates a PDF document and returns metadata (multipart/form-data)
+ ///
+ /// PDF file to validate
+ /// Cancellation token
+ /// PDF metadata (page count, file size, PDF version, attachments)
+ /// PDF is valid, metadata returned
+ /// Invalid PDF or file format
+ /// Internal server error during validation
+ [HttpPost("validate")]
+ [Consumes("multipart/form-data")]
+ [ProducesResponseType(typeof(PdfValidationResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
+ public async Task ValidateFromFile(
+ IFormFile file,
+ CancellationToken cancellationToken)
+ {
+ if (file == null || file.Length == 0)
+ {
+ return BadRequest(new ProblemDetails
+ {
+ Title = "Invalid file",
+ Detail = "File is required and cannot be empty",
+ Status = StatusCodes.Status400BadRequest
+ });
+ }
+
+ // Convert IFormFile to byte array
+ using var memoryStream = new MemoryStream();
+ await file.CopyToAsync(memoryStream, cancellationToken);
+ byte[] pdfBytes = memoryStream.ToArray();
+
+ // Direct pass-through to MediatR
+ var query = new ValidatePdfQuery { PdfBytes = pdfBytes };
+ var result = await Mediator.Send(query, cancellationToken);
+
+ return Ok(result);
+ }
+
+ ///
+ /// Validates a PDF document and returns metadata (Base64 JSON)
+ ///
+ /// PDF as Base64 string
+ /// Cancellation token
+ /// PDF metadata (page count, file size, PDF version, attachments)
+ /// PDF is valid, metadata returned
+ /// Invalid PDF or Base64 format
+ /// Internal server error during validation
+ [HttpPost("validate")]
+ [Consumes("application/json")]
+ [ProducesResponseType(typeof(PdfValidationResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
+ public async Task ValidateFromBase64(
+ [FromBody] ValidatePdfQuery query,
+ CancellationToken cancellationToken)
+ {
+ // Direct pass-through to MediatR
+ var result = await Mediator.Send(query, cancellationToken);
+
+ return Ok(result);
+ }
+}
diff --git a/DocumentOperator.API/Controllers/SwissQrCodeController.cs b/DocumentOperator.API/Controllers/SwissQrCodeController.cs
new file mode 100644
index 0000000..43a4630
--- /dev/null
+++ b/DocumentOperator.API/Controllers/SwissQrCodeController.cs
@@ -0,0 +1,94 @@
+using DocumentOperator.Application.Common.DTOs;
+using DocumentOperator.Application.SwissQrCode.Queries;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace DocumentOperator.API.Controllers;
+
+///
+/// Swiss QR Code extraction operations
+///
+[ApiController]
+[Route("api/pdf/qr-code")]
+[Produces("application/json")]
+public class SwissQrCodeController(IMediator Mediator) : ControllerBase
+{
+ ///
+ /// Extracts Swiss QR Code from the last page of a PDF document (multipart/form-data)
+ ///
+ /// PDF file containing Swiss QR Code
+ /// Optional references (comma-separated)
+ /// Cancellation token
+ /// Swiss QR Code data (IBAN, amount, creditor, debtor, reference, etc.)
+ /// Swiss QR Code extracted successfully
+ /// Invalid PDF or file format
+ /// No Swiss QR Code found on the last page
+ /// Internal server error during extraction
+ [HttpPost("extract-swiss")]
+ [Consumes("multipart/form-data")]
+ [ProducesResponseType(typeof(SwissQrCodeExtractionResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
+ public async Task ExtractFromFile(
+ IFormFile file,
+ [FromForm] string? references,
+ CancellationToken cancellationToken)
+ {
+ if (file == null || file.Length == 0)
+ {
+ return BadRequest(new ProblemDetails
+ {
+ Title = "Invalid file",
+ Detail = "File is required and cannot be empty",
+ Status = StatusCodes.Status400BadRequest
+ });
+ }
+
+ // Convert IFormFile to byte array
+ using var memoryStream = new MemoryStream();
+ await file.CopyToAsync(memoryStream, cancellationToken);
+ byte[] pdfBytes = memoryStream.ToArray();
+
+ // Parse references (comma-separated or empty)
+ var referencesList = string.IsNullOrWhiteSpace(references)
+ ? new List()
+ : references.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
+
+ // Direct pass-through to MediatR
+ var query = new ExtractSwissQrCodeQuery
+ {
+ PdfBytes = pdfBytes,
+ References = referencesList
+ };
+ var result = await Mediator.Send(query, cancellationToken);
+
+ return Ok(result);
+ }
+
+ ///
+ /// Extracts Swiss QR Code from the last page of a PDF document (Base64 JSON)
+ ///
+ /// References array + PDF as Base64 string
+ /// Cancellation token
+ /// Swiss QR Code data (IBAN, amount, creditor, debtor, reference, etc.)
+ /// Swiss QR Code extracted successfully
+ /// Invalid PDF or Base64 format
+ /// No Swiss QR Code found on the last page
+ /// Internal server error during extraction
+ [HttpPost("extract-swiss")]
+ [Consumes("application/json")]
+ [ProducesResponseType(typeof(SwissQrCodeExtractionResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
+ public async Task ExtractFromBase64(
+ [FromBody] ExtractSwissQrCodeQuery query,
+ CancellationToken cancellationToken)
+ {
+ // Direct pass-through to MediatR
+ var result = await Mediator.Send(query, cancellationToken);
+
+ return Ok(result);
+ }
+}
diff --git a/DocumentOperator.API/Endpoints/v1/DocumentEndpoints.cs b/DocumentOperator.API/Endpoints/v1/DocumentEndpoints.cs
deleted file mode 100644
index 48b1f97..0000000
--- a/DocumentOperator.API/Endpoints/v1/DocumentEndpoints.cs
+++ /dev/null
@@ -1,165 +0,0 @@
-using DocumentOperator.Application.Common.DTOs;
-using DocumentOperator.Application.Features.Documents.ExtractSwissQrCode;
-using DocumentOperator.Application.Features.Documents.ValidatePdf;
-using DocumentOperator.Domain.Models.ValueObjects;
-using MediatR;
-using Microsoft.AspNetCore.Mvc;
-
-namespace DocumentOperator.API.Endpoints.v1;
-
-///
-/// Document endpoints (Minimal API)
-///
-public static class DocumentEndpoints
-{
- ///
- /// Maps all document-related endpoints
- ///
- public static void MapDocumentEndpoints(this IEndpointRouteBuilder app)
- {
- var group = app.MapGroup("/api/v1/documents")
- .WithTags("Documents");
-
- // POST /api/v1/documents/validate
- group.MapPost("/validate", ValidatePdf)
- .WithName("ValidatePdf")
- .WithSummary("Validates a PDF document and returns metadata")
- .WithDescription("Validates the PDF format and extracts metadata (page count, file size, PDF version, attachments)")
- .Produces(StatusCodes.Status200OK)
- .Produces(StatusCodes.Status400BadRequest)
- .Produces(StatusCodes.Status500InternalServerError);
-
- // POST /api/v1/documents/extract-swiss-qr-code
- group.MapPost("/extract-swiss-qr-code", ExtractSwissQrCode)
- .WithName("ExtractSwissQrCode")
- .WithSummary("Extracts Swiss QR Code from the last page of a PDF document")
- .WithDescription(@"Extracts and parses a Swiss QR Code (Swiss QR Bill Standard 2.0) from the last page of a PDF document.
-
-**Requirements:**
-- PDF must contain a valid Swiss QR Code on the last page
-- QR Code must conform to Swiss QR Bill Standard 2.0
-- References array is required (can be empty)
-
-**Returns:**
-- All QR code fields (IBAN, amount, creditor, debtor, reference, etc.)
-- References array (passed through from request)
-
-**Use Case:**
-Extract payment information from Swiss QR invoices for automated processing.")
- .Produces(StatusCodes.Status200OK)
- .Produces(StatusCodes.Status400BadRequest)
- .Produces(StatusCodes.Status404NotFound)
- .Produces(StatusCodes.Status500InternalServerError);
- }
-
- ///
- /// Validates a PDF document and returns metadata
- ///
- /// PDF as Base64 string
- /// MediatR instance
- /// Cancellation token
- /// PDF metadata (page count, file size, etc.)
- /// PDF is valid, metadata returned
- /// Invalid PDF or Base64 format
- /// Internal server error during validation
- private static async Task ValidatePdf(
- ValidatePdfRequest request,
- IMediator mediator,
- CancellationToken cancellationToken)
- {
- // DTO → Query (Value Objects erstellen - kann DomainValidationException werfen!)
- var query = new ValidatePdfQuery(
- Base64String.Create(request.Base64Pdf)
- );
-
- // MediatR Handler aufrufen (ValidationBehavior → Handler)
- var metadata = await mediator.Send(query, cancellationToken);
-
- // PdfMetadata → Response DTO
- var response = new ValidatePdfResponse(
- metadata.PageCount,
- metadata.FileSizeBytes,
- metadata.FileSizeMB,
- metadata.PdfVersion,
- metadata.HasAttachments,
- metadata.AttachmentCount
- );
-
- return Results.Ok(response);
- }
-
- ///
- /// Extracts Swiss QR Code from the last page of a PDF document
- ///
- /// References array + PDF as Base64 string
- /// MediatR instance
- /// Cancellation token
- /// References (passed through) + Swiss QR Code data
- /// Swiss QR Code extracted successfully
- /// Invalid PDF or Base64 format
- /// No Swiss QR Code found on the last page
- /// Internal server error during extraction
- private static async Task ExtractSwissQrCode(
- ExtractSwissQrCodeRequest request,
- IMediator mediator,
- CancellationToken cancellationToken)
- {
- // DTO → Query (Value Objects erstellen)
- var query = new ExtractSwissQrCodeQuery(
- References: request.References,
- PdfContent: Base64String.Create(request.Base64Pdf)
- );
-
- // MediatR Handler aufrufen
- var result = await mediator.Send(query, cancellationToken);
-
- // Map Domain Value Object → DTO
- var response = new ExtractSwissQrCodeResponse(
- References: result.References,
- QrCodeData: MapQrCodeDataToDto(result.QrCodeData)
- );
-
- return Results.Ok(response);
- }
-
- ///
- /// Maps SwissQrCodeData domain value object to DTO
- ///
- private static SwissQrCodeDataDto MapQrCodeDataToDto(Domain.ValueObjects.SwissQrCodeData qrCodeData)
- {
- return new SwissQrCodeDataDto(
- QrType: qrCodeData.QrType,
- Version: qrCodeData.Version,
- CodingType: qrCodeData.CodingType,
- Iban: qrCodeData.Iban,
- Creditor: MapAddressToDto(qrCodeData.Creditor),
- UltimateCreditor: qrCodeData.UltimateCreditor != null ? MapAddressToDto(qrCodeData.UltimateCreditor) : null,
- Amount: qrCodeData.Amount,
- Currency: qrCodeData.Currency,
- UltimateDebtor: qrCodeData.UltimateDebtor != null ? MapAddressToDto(qrCodeData.UltimateDebtor) : null,
- ReferenceType: qrCodeData.ReferenceType,
- Reference: qrCodeData.Reference,
- UnstructuredMessage: qrCodeData.UnstructuredMessage,
- BillInformation: qrCodeData.BillInformation,
- AlternativeProcedureParameters: qrCodeData.AlternativeProcedureParameters
- );
- }
-
- ///
- /// Maps AddressData domain value object to DTO
- ///
- private static AddressDataDto MapAddressToDto(Domain.ValueObjects.AddressData address)
- {
- return new AddressDataDto(
- AddressType: address.AddressType,
- Name: address.Name,
- Street: address.Street,
- BuildingNumber: address.BuildingNumber,
- AddressLine1: address.AddressLine1,
- AddressLine2: address.AddressLine2,
- PostalCode: address.PostalCode,
- City: address.City,
- Country: address.Country
- );
- }
-}
diff --git a/DocumentOperator.API/ROADMAP.md b/DocumentOperator.API/ROADMAP.md
deleted file mode 100644
index d8360ec..0000000
--- a/DocumentOperator.API/ROADMAP.md
+++ /dev/null
@@ -1,1101 +0,0 @@
-# ?? DocumentOperator - Project Roadmap (Feature-Driven Development)
-
-> **Last Updated:** 17.01.2025 | **Status:** In Development | **Current Feature:** Feature 1 - ValidatePDF ? ABGESCHLOSSEN!
-
----
-
-## ?? NEW APPROACH: Feature-by-Feature Development
-
-**Was hat sich geändert?**
-
-? **Feature-Driven Development** statt Layer-by-Layer
-- Jedes Feature wird KOMPLETT umgesetzt (Domain ? Infrastructure ? Application ? API ? Tests ? Swagger)
-- Feature ist erst "DONE" wenn es im Swagger testbar ist
-- Dann nächstes Feature
-
-? **Kleine Schritte** (1 Layer pro Step)
-- Besser überschaubar
-- Weniger Merge-Konflikte
-- Schnelleres Feedback
-
-? **Multi-Tenancy & Cross-Cutting Concerns später**
-- Erst alle synchronen Features implementieren
-- Dann Multi-Tenancy für ALLE Endpoints (kein Wiederholungsaufwand!)
-- Dann Health Checks, Polly, Logging (einmal für alle!)
-
----
-
-## ?? TABLE OF CONTENTS
-
-1. [Feature Roadmap](#feature-roadmap)
-2. [Project Overview](#project-overview)
-3. [Architecture & Design Decisions](#architecture--design-decisions)
-4. [Technology Stack](#technology-stack)
-5. [Project Structure](#project-structure)
-6. [Testing Strategy](#testing-strategy)
-7. [Current Status](#current-status)
-8. [Key Learnings & Decisions](#key-learnings--decisions)
-
----
-
-## ?? FEATURE ROADMAP
-
-### ?? Feature Overview
-
-| Feature | Type | Status | Swagger Testbar? |
-|---------|------|--------|------------------|
-| **1. ValidatePDF** | Synchron | ? Abgeschlossen | ? |
-| **2. ExtractSwissQrCode** | Synchron | ? Abgeschlossen | ? |
-| **3. ExtractAttachments** | Synchron | ? Pending | ? |
-| **4. ApplyStamp** | Synchron | ? Pending | ? |
-| **5. EmbedCertificate** | Synchron | ? Pending | ? |
-| **6. ConcatenatePDFs** | Asynchron | ? Pending | ? |
-
-### ?? Cross-Cutting Concerns (nach Features 1-4)
-
-| Concern | Status |
-|---------|--------|
-| **Multi-Tenancy** (X-API-Key Header) | ? Pending |
-| **Health Checks** (/health Endpoint) | ? Pending |
-| **Polly Resilience** (Retry, Circuit Breaker) | ? Pending |
-| **Logging & Monitoring** (Correlation IDs, Seq) | ? Pending |
-
----
-
-## ?? FEATURE 1: ValidatePDF (Synchron) - **In Progress**
-
-**Was macht dieses Feature?**
-- Client sendet PDF als Base64 (JSON)
-- API validiert PDF
-- API gibt Metadaten zurück (Seitenzahl, Dateigröße, PDF-Version, Anhänge)
-
-**Endpoint:**
-```
-POST /api/v1/documents/validate
-Request: { "base64Pdf": "JVBERi0xLjQK..." }
-Response: { "pageCount": 5, "fileSizeBytes": 1024, "pdfVersion": "1.4", "hasAttachments": false }
-```
-
----
-
-### ? Step 1.0: Foundation (ABGESCHLOSSEN)
-
-**Was wurde bereits erstellt:**
-- ? Domain Layer (Exceptions, Enums, Value Objects)
-- ? Infrastructure Layer (`DevExpressPdfProcessor.ValidateAsync` - FERTIG!)
-- ? Tests für Infrastructure (`DevExpressPdfProcessorTests.cs` - 6 Tests)
-
-**Was wir wiederverwenden:**
-- `Base64String` Value Object (Domain)
-- `PdfMetadata` Value Object (Domain)
-- `IPdfProcessor` Interface (Application)
-- `DevExpressPdfProcessor.ValidateAsync` (Infrastructure)
-
----
-
-### ? Step 1.1: Application Layer (MediatR Setup + ValidatePDF Feature) - **ABGESCHLOSSEN**
-
-**Ziel:** MediatR + FluentValidation + ValidatePDF Handler
-
-**Was wurde erstellt:**
-
-#### 1.1.1: MediatR Setup
-- ? **Datei:** `Application/DependencyInjection.cs`
- - Registriert MediatR
- - Registriert FluentValidation
- - Registriert Pipeline Behaviors (Validation + Logging)
-
-#### 1.1.2: Pipeline Behaviors
-- ? **Datei:** `Application/Common/Behaviors/ValidationBehavior.cs`
- - Führt FluentValidation automatisch aus (vor jedem Handler)
- - Wirft `ValidationException` bei Fehler
-
-- ? **Datei:** `Application/Common/Behaviors/LoggingBehavior.cs`
- - Loggt jeden Request (mit Performance-Tracking)
- - Nutzt `ILogger` (Clean Architecture konform!)
-
-#### 1.1.3: ValidatePDF Feature (Vertical Slice)
-- ? **Ordner:** `Application/Features/Documents/ValidatePdf/`
-
-- ? **Datei:** `ValidatePdfQuery.cs`
- ```csharp
- public record ValidatePdfQuery(Base64String PdfContent) : IRequest;
- ```
-
-- ? **Datei:** `ValidatePdfHandler.cs`
- ```csharp
- public class ValidatePdfHandler : IRequestHandler
- {
- private readonly IPdfProcessor _processor;
-
- public async Task Handle(ValidatePdfQuery query, CancellationToken ct)
- {
- byte[] bytes = query.PdfContent.ToByteArray();
- return await _processor.ValidateAsync(bytes);
- }
- }
- ```
-
-- ? **Datei:** `ValidatePdfValidator.cs`
- ```csharp
- public class ValidatePdfValidator : AbstractValidator
- {
- public ValidatePdfValidator()
- {
- RuleFor(x => x.PdfContent).NotNull().WithMessage("PDF content is required");
- }
- }
- ```
-
-#### 1.1.4: DTOs
-- ? **Ordner:** `Application/Common/DTOs/`
-
-- ? **Datei:** `ValidatePdfRequest.cs`
- ```csharp
- public record ValidatePdfRequest(string Base64Pdf);
- ```
-
-- ? **Datei:** `ValidatePdfResponse.cs`
- ```csharp
- public record ValidatePdfResponse(
- int PageCount,
- long FileSizeBytes,
- double FileSizeMB,
- string PdfVersion,
- bool HasAttachments,
- int AttachmentCount);
- ```
-
-#### 1.1.5: Tests
-- ? **Datei:** `Tests/Unit/Application/Features/ValidatePdf/ValidatePdfHandlerTests.cs`
- - ? Test: `Handle_ValidPdf_ReturnsPdfMetadata`
- - ? Test: `Handle_PdfProcessorThrowsException_PropagatesException`
-
-**Akzeptanzkriterien:**
-- ? Build erfolgreich
-- ? Tests grün (alle 2 Tests)
-- ? MediatR Pipeline funktioniert (Validation + Logging)
-
----
-
-### ? Step 1.2: API Layer (Endpoint + Exception Middleware) - **ABGESCHLOSSEN**
-
-**Ziel:** HTTP Endpoint + zentrale Exception Handling
-
-**Was wurde erstellt:**
-
-#### 1.2.1: Exception Handling Middleware
-- ? **Datei:** `API/Middleware/ExceptionHandlingMiddleware.cs`
- - Fängt alle Exceptions
- - Mappt zu HTTP Status Codes (ValidationException ? 400, DomainValidationException ? 400, NotFoundException ? 404, PdfProcessingException ? 500)
- - Gibt RFC 7807 Problem Details zurück
-
-#### 1.2.2: Minimal API Endpoint
-- ? **Datei:** `API/Endpoints/v1/DocumentEndpoints.cs`
- - POST /api/v1/documents/validate
- - Nutzt MediatR (ValidatePdfQuery ? ValidatePdfHandler)
- - Returns ValidatePdfResponse (200) oder ProblemDetails (400, 500)
-
-#### 1.2.3: Infrastructure DependencyInjection
-- ? **Datei:** `Infrastructure/DependencyInjection.cs`
- - Registriert IPdfProcessor ? DevExpressPdfProcessor
-
-#### 1.2.4: Program.cs Updates
-- ? Application Layer registriert (AddApplication)
-- ? Infrastructure Layer registriert (AddInfrastructure)
-- ? Exception Middleware registriert (FIRST in pipeline!)
-- ? Endpoints registriert (MapDocumentEndpoints)
-
-#### 1.2.5: Integration Tests
-- ? **Datei:** `Tests/Integration/API/DocumentEndpointsTests.cs`
- - ? Test: `POST_ValidatePdf_ValidPdf_Returns200`
- - ? Test: `POST_ValidatePdf_InvalidBase64_Returns400`
- - ? Test: `POST_ValidatePdf_EmptyPdf_Returns400`
-
-**Akzeptanzkriterien:**
-- ? Build erfolgreich
-- ? Integration Tests grün (alle 3 Tests)
-- ? Exception Middleware funktioniert
-- ? Endpoint gibt korrekte HTTP Status Codes zurück
-
----
-
-### ? Step 1.3: Swagger Dokumentation - **ABGESCHLOSSEN**
-
-**Ziel:** API-Dokumentation + Swagger UI testbar
-
-**Was wurde erstellt:**
-
-#### 1.3.1: Swagger Configuration
-- ? **Datei:** `API/Configuration/SwaggerConfiguration.cs`
- - `AddSwaggerDocumentation()` Extension Method
- - Swagger mit XML Comments konfiguriert
- - API-Titel, Version, Beschreibung gesetzt
-
-#### 1.3.2: XML Comments aktiviert
-- ? **Datei:** `API/DocumentOperator.API.csproj`
- ```xml
-
- true
-
- ```
-
-#### 1.3.3: Endpoint dokumentiert
-- ? **Datei:** `API/Endpoints/v1/DocumentEndpoints.cs`
- - XML Comments für `ValidatePdf` Methode
- - Swagger-Annotationen (`.WithSummary()`, `.WithDescription()`, `.Produces<>()`)
-
-#### 1.3.4: DTOs dokumentiert
-- ? **Datei:** `Application/Common/DTOs/ValidatePdfRequest.cs`
- - XML Comments für Request-Schema
-
-- ? **Datei:** `Application/Common/DTOs/ValidatePdfResponse.cs`
- - XML Comments für Response-Schema
- - `FileSizeMB` Property hinzugefügt
-
-#### 1.3.5: Program.cs aktualisiert
-- ? **Datei:** `API/Program.cs`
- - `builder.Services.AddSwaggerDocumentation()` statt `AddSwaggerGen()`
- - `using DocumentOperator.API.Configuration;` hinzugefügt
-
-**Akzeptanzkriterien:**
-- ? Build erfolgreich
-- ? Alle Tests grün (11/11 Tests)
-- ? XML-Dokumentation wird generiert (`DocumentOperator.API.xml`)
-- ? Swagger UI zeigt Endpoint `/api/v1/documents/validate` mit Dokumentation
-- ? Request/Response-Schemas sind dokumentiert
-- ? Endpoint ist im Swagger UI testbar
-
----
-
-### ? Feature 1 ABGESCHLOSSEN!
-
-**Was haben wir erreicht?**
-- ? ValidatePDF Feature komplett implementiert (Domain ? Infrastructure ? Application ? API ? Tests ? Swagger)
-- ? Endpoint ist im Swagger UI testbar
-- ? Unit Tests + Integration Tests grün (11/11)
-- ? Clean Architecture eingehalten
-- ? TDD angewendet
-- ? Swagger-Dokumentation vollständig
-
-**Nächstes Feature:**
-? **Feature 3: ExtractAttachments**
-
----
-
-## ?? FEATURE 2: ExtractSwissQrCode (Synchron) - **ABGESCHLOSSEN**
-
-**Was macht dieses Feature?**
-- Client sendet PDF als Base64 + Referenzen (Array von Strings)
-- API extrahiert Swiss QR Code von der **letzten Seite** des PDFs
-- API gibt Referenzen + alle QR Code Felder zurück (Swiss QR Bill Standard 2.0)
-
-**Endpoint:**
-```
-POST /api/v1/documents/extract-swiss-qr-code
-Request:
-{
- "references": ["REF-001", "REF-002"],
- "base64Pdf": "JVBERi0xLjQK..."
-}
-
-Response:
-{
- "references": ["REF-001", "REF-002"],
- "qrCodeData": {
- "qrType": "SPC",
- "version": "0200",
- "codingType": "1",
- "iban": "CH4431999123000889012",
- "creditor": {
- "name": "Robert Schneider AG",
- "addressType": "S",
- "street": "Rue du Lac",
- "buildingNumber": "1268",
- "postalCode": "2501",
- "city": "Biel",
- "country": "CH"
- },
- "ultimateCreditor": null,
- "amount": 1949.75,
- "currency": "CHF",
- "ultimateDebtor": {
- "name": "Pia-Maria Rutschmann-Schnyder",
- "addressType": "S",
- "street": "Grosse Marktgasse",
- "buildingNumber": "28",
- "postalCode": "9400",
- "city": "Rorschach",
- "country": "CH"
- },
- "referenceType": "QRR",
- "reference": "210000000003139471430009017",
- "unstructuredMessage": "Rechnung vom 15.01.2025",
- "billInformation": "//S1/01/...",
- "alternativeProcedureParameters": ["Name AV1: UV;UltraPay005;12345", "Name AV2: XY;XYService;54321"]
- }
-}
-```
-
-**Technologie:**
-- **DevExpress PDF Document API** (PDF-Zugriff, letzte Seite, QR Code Image)
-- **Codecrete.SwissQRBill.Generator** (Swiss QR Code Parsing - Standard 2.0)
-
-**Steps:**
-- ? Step 2.1: Domain Layer (SwissQrCodeData Value Object)
-- ? Step 2.2: Infrastructure Layer (IQrCodeProcessor + DevExpressSwissQrCodeProcessor)
-- ? Step 2.3: Application Layer (ExtractSwissQrCodeQuery + Handler + Validator)
-- ? Step 2.4: API Layer (Endpoint + Integration Tests)
-- ? Step 2.5: Swagger Dokumentation
-
-**Akzeptanzkriterien:**
-- ? QR Code wird von letzter Seite extrahiert
-- ? Alle Swiss QR Bill Felder werden geparst (Standard 2.0)
-- ? Referenzen werden durchgeschliffen (Echo)
-- ? Fehler wenn kein QR Code gefunden
-- ? Swagger-testbar
-- ? Tests grün (19/19)
-
-### ? Feature 2 ABGESCHLOSSEN!
-
-**Was haben wir erreicht?**
-- ? ExtractSwissQrCode Feature komplett implementiert (Domain ? Infrastructure ? Application ? API ? Tests ? Swagger)
-- ? Endpoint ist im Swagger UI testbar: `POST /api/v1/documents/extract-swiss-qr-code`
-- ? Unit Tests + Integration Tests grün (19/19)
-- ? 3 Libraries integriert: DevExpress PDF, ZXing.Net, Codecrete.SwissQRBill.Generator
-- ? Vollständige Swiss QR Bill Standard 2.0 Unterstützung
-- ? Clean Architecture eingehalten
-- ? Swagger-Dokumentation vollständig
-
-**Nächstes Feature:**
-? **Feature 3: ExtractAttachments**
-
----
-
-## ?? FEATURE 3: ExtractAttachments (Synchron) - **PENDING**
-
-**Was macht dieses Feature?**
-- Client sendet PDF als Base64 (JSON)
-- API extrahiert eingebettete Anhänge
-- API gibt Anhänge als Base64 zurück (oder Download-Links)
-
-**Endpoint:**
-```
-POST /api/v1/documents/extract-attachments
-Request: { "base64Pdf": "JVBERi0xLjQK..." }
-Response: { "attachments": [{ "name": "invoice.xml", "base64Content": "..." }] }
-```
-
-**Steps:**
-- ?? Step 3.1: Infrastructure Layer (DevExpressPdfProcessor.ExtractAttachmentsAsync)
-- ?? Step 3.2: Application Layer (ExtractAttachmentsCommand + Handler + Validator)
-- ?? Step 3.3: API Layer (Endpoint)
-- ?? Step 3.4: Swagger Dokumentation
-
----
-
-## ?? FEATURE 4: ApplyStamp (Synchron) - **PENDING**
-
-**Was macht dieses Feature?**
-- Client sendet PDF + Stamp-Konfiguration (Text, Position)
-- API fügt Stamp hinzu (Logo, Text, Wasserzeichen)
-- API gibt gestempeltes PDF zurück
-
-**Endpoint:**
-```
-POST /api/v1/documents/apply-stamp
-Request: { "base64Pdf": "...", "text": "CONFIDENTIAL", "position": "TopRight" }
-Response: { "base64Pdf": "JVBERi0xLjQK..." }
-```
-
-**Steps:**
-- ?? Step 4.1: Infrastructure Layer (DevExpressPdfProcessor.ApplyStampAsync)
-- ?? Step 4.2: Application Layer (ApplyStampCommand + Handler + Validator)
-- ?? Step 4.3: API Layer (Endpoint)
-- ?? Step 4.4: Swagger Dokumentation
-
----
-
-## ?? FEATURE 5: EmbedCertificate (Synchron) - **PENDING**
-
-**Was macht dieses Feature?**
-- Client sendet PDF + Zertifikat (PFX als Base64)
-- API bettet Zertifikat als Attachment ein
-- API gibt PDF mit Zertifikat zurück
-
-**Endpoint:**
-```
-POST /api/v1/documents/embed-certificate
-Request: { "base64Pdf": "...", "base64Certificate": "..." }
-Response: { "base64Pdf": "JVBERi0xLjQK..." }
-```
-
-**Steps:**
-- ?? Step 5.1: Infrastructure Layer (DevExpressPdfProcessor.EmbedCertificateAsync)
-- ?? Step 5.2: Application Layer (EmbedCertificateCommand + Handler + Validator)
-- ?? Step 5.3: API Layer (Endpoint)
-- ?? Step 5.4: Swagger Dokumentation
-
----
-
-## ?? FEATURE 6: ConcatenatePDFs (Asynchron) - **PENDING**
-
-**Was macht dieses Feature?**
-- Client sendet mehrere PDFs (Array von Base64)
-- API startet asynchronen Job (gibt JobId zurück)
-- Client pollt Job-Status
-- Wenn fertig: Client lädt Ergebnis herunter
-
-**Endpoints:**
-```
-POST /api/v1/documents/concatenate (Async)
-Request: { "pdfFiles": ["JVBERi0x...", "JVBERi0y..."] }
-Response: { "jobId": "abc-123", "status": "Pending" }
-
-GET /api/v1/jobs/{jobId}
-Response: { "jobId": "abc-123", "status": "Processing", "progress": 50 }
-
-GET /api/v1/jobs/{jobId}/download
-Response: PDF-Datei (Binary)
-```
-
-**Steps:**
-- ?? Step 6.1: Infrastructure Layer (In-Memory Queue + Background Worker)
-- ?? Step 6.2: Application Layer (SubmitConcatenateJobCommand + GetJobStatusQuery)
-- ?? Step 6.3: API Layer (Async Endpoints)
-- ?? Step 6.4: Swagger Dokumentation
-
----
-
-## ?? CROSS-CUTTING CONCERNS
-
-**Nach Features 1-4 abgeschlossen:**
-
-### ?? Multi-Tenancy (X-API-Key Header)
-
-**Was wird gebaut:**
-- EF Core + SQLite (Tenant-Datenbank)
-- Redis Cache (API-Key Lookups)
-- TenantResolutionMiddleware (X-API-Key ? Tenant)
-- BCrypt API-Key Hashing
-- Admin API (Tenant CRUD)
-
-**Steps:**
-- ?? Step MT.1: EF Core Setup (Entities, DbContext, Migrations)
-- ?? Step MT.2: TenantResolutionMiddleware
-- ?? Step MT.3: Redis Cache Integration
-- ?? Step MT.4: Admin API (Tenant Management)
-- ?? Step MT.5: Alle Endpoints mit X-API-Key absichern
-
----
-
-### ?? Health Checks
-
-**Was wird gebaut:**
-- `/health` Endpoint (Liveness/Readiness Probes)
-- DevExpressPdfHealthCheck (Smoke Test)
-- Database Health Check (SQLite)
-- Redis Health Check
-
-**Steps:**
-- ?? Step HC.1: DevExpressPdfHealthCheck
-- ?? Step HC.2: Database Health Check
-- ?? Step HC.3: Redis Health Check (optional)
-
----
-
-### ??? Polly Resilience
-
-**Was wird gebaut:**
-- Retry Policy (3x mit Exponential Backoff)
-- Circuit Breaker (nach 5 Fehlern 30s öffnen)
-- Timeout Policy (30s max)
-
-**Steps:**
-- ?? Step PR.1: Polly Policies in DevExpressPdfProcessor
-- ?? Step PR.2: Logging für Resilience Events
-
----
-
-### ?? Logging & Monitoring
-
-**Was wird gebaut:**
-- Correlation IDs (X-Correlation-ID Header)
-- Seq Sink (Log-Browsing UI)
-- File Logging (Production)
-- LoggingBehavior (MediatR Pipeline)
-
-**Steps:**
-- ?? Step LM.1: CorrelationIdMiddleware
-- ?? Step LM.2: Seq Sink konfigurieren
-- ?? Step LM.3: File Logging konfigurieren
-- ?? Step LM.4: LoggingBehavior erweitern (Performance-Tracking)
-
----
-
-## ?? PROJECT OVERVIEW
-
-### Vision & Purpose
-
-**DocumentOperator** ist ein zentralisierter REST API Service für PDF-Dokumenten-Operationen in einer Multi-Tenant DMS-Umgebung.
-
-### Problem Statement
-
-**Aktuell:**
-- Verschiedene DMS-Kunden bei unterschiedlichen Mandanten
-- Jede Anwendung implementiert PDF-Operationen redundant
-- Keine zentrale Stelle für Dokumenten-Verarbeitung
-- Wartungsaufwand multipliziert sich mit jeder Anwendung
-
-**Lösung:**
-- **Ein** zentraler Service für alle PDF-Operationen
-- Wiederverwendbar über HTTP REST API
-- Mandantenfähig (Multi-Tenancy - später!)
-- Wartbar an einer Stelle
-
----
-
-## ??? ARCHITECTURE & DESIGN DECISIONS
-
-### Clean Architecture (Pragmatisch!)
-
-Wir verwenden **Clean Architecture** mit 4 Layers - **ABER: pragmatisch, nicht dogmatisch!**
-
-```
-?????????????????????????????????????
-? API Layer (Endpoints) ? ? HTTP Entry Point
-?????????????????????????????????????
-? Application Layer (Use Cases) ? ? MediatR Handlers, DTOs
-?????????????????????????????????????
-? Infrastructure Layer (Tech Stack) ? ? DevExpress, File I/O
-?????????????????????????????????????
-? Domain Layer (MINIMAL!) ? ? Nur Enums + Value Objects
-?????????????????????????????????????
-```
-
-#### Dependency Rule
-
-**Abhängigkeiten zeigen immer nach innen:**
-
-```
-API ? Application ? Domain
-API ? Infrastructure ? Domain
-Infrastructure ? Application (für Interfaces)
-
-Domain ? NICHTS! (No External Dependencies)
-Application ? NUR Domain
-```
-
-**Warum Clean Architecture?**
-- ? Testbarkeit (Application Layer kann Services mocken)
-- ? Austauschbarkeit (DevExpress ? anderes PDF-Lib ohne Application zu ändern)
-- ? Separation of Concerns (jede Schicht hat klare Verantwortung)
-
-**ABER:**
-- ? Kein Overengineering (nur was wir wirklich brauchen!)
-- ? Keine spekulativen Abstraktionen (erst wenn 2. Use Case es braucht)
-- ? Keine unnötigen Klassen (YAGNI - You Ain't Gonna Need It)
-
----
-
-### CQRS with MediatR
-
-**Pattern:** Command Query Responsibility Segregation
-
-**Warum MediatR?**
-- ? Klare Trennung: 1 Command/Query = 1 Handler = 1 Verantwortung
-- ? Testbarkeit (Handler kann isoliert getestet werden)
-- ? Pipeline Behaviors (Validation, Logging zentral)
-- ? Kein aufgeblähter Service mit 20 Methoden
-
-**CQRS in unserem Kontext:**
-- **Command:** Ändert Daten (ApplyStamp, EmbedCertificate, etc.)
-- **Query:** Liest Daten (ValidatePdf ? gibt nur Metadata zurück)
-
-**Beispiel:**
-```csharp
-// Query (Read-Only)
-public record ValidatePdfQuery(Base64String PdfContent) : IRequest;
-
-// Handler
-public class ValidatePdfHandler : IRequestHandler
-{
- private readonly IPdfProcessor _processor;
-
- public async Task Handle(ValidatePdfQuery query, CancellationToken ct)
- {
- byte[] bytes = query.PdfContent.ToByteArray();
- var metadata = await _processor.ValidateAsync(bytes);
- return metadata;
- }
-}
-```
-
----
-
-### Vertical Slice Architecture
-
-**Statt Horizontal Layers** (Commands/, Handlers/, Validators/):
-
-```
-? Horizontal (Schlecht für Wartung):
-Application/
-??? Commands/
-? ??? ValidatePdfCommand.cs
-? ??? ProcessDocumentCommand.cs
-??? Handlers/
-? ??? ValidatePdfHandler.cs
-? ??? ProcessDocumentHandler.cs
-??? Validators/
- ??? ValidatePdfValidator.cs
- ??? ProcessDocumentValidator.cs
-```
-
-**Nutzen wir Vertical Slices** (pro Feature alles zusammen):
-
-```
-? Vertical (Gut für Wartung):
-Features/
-??? ValidatePdf/
-? ??? ValidatePdfQuery.cs
-? ??? ValidatePdfHandler.cs
-? ??? ValidatePdfValidator.cs
-??? ProcessDocument/
- ??? ProcessDocumentCommand.cs
- ??? ProcessDocumentHandler.cs
- ??? ProcessDocumentValidator.cs
-```
-
-**Vorteile:**
-- ? Zusammengehöriger Code ist zusammen (Cohesion)
-- ? Einfacher zu finden ("Wo ist ValidatePdf?" ? ein Ordner!)
-- ? Einfacher zu ändern (alle Dateien im gleichen Ordner)
-- ? Weniger Merge-Konflikte im Team
-
----
-
-### Exception-based Error Handling
-
-**Entscheidung:** Keine Result Pattern Library
-
-**Stattdessen:**
-1. **FluentValidation** für Input-Validierung (DTO-Ebene)
-2. **Domain Exceptions** für fachliche Fehler
-3. **Zentrale Exception Handling Middleware** im API Layer
-
-**Warum Exception-basiert?**
-- ? Einfacherer Code (kein `if (result.IsSuccess)` überall)
-- ? Weniger Boilerplate (kein Result Wrapping)
-- ? Standard .NET Exception-Flow (jeder kennt es)
-- ? Zentrales Error Handling = wartbar an **einer** Stelle
-
-**Flow:**
-```
-HTTP Request
- ?
-FluentValidation (MediatR ValidationBehavior)
- ? Bei Fehler: ValidationException ? Middleware ? HTTP 400
- ?
-Handler
- ? Bei Fehler: DomainException ? Middleware ? HTTP 400/404/500
- ?
-Middleware (Exception Handler)
- ? Mappt Exception Type ? HTTP Status Code
- ? Loggt Exception (Serilog)
- ? Gibt Problem Details (RFC 7807) zurück
- ?
-HTTP Response (JSON)
-```
-
----
-
-## ?? TECHNOLOGY STACK
-
-### Core Framework
-
-| Technology | Version | Purpose |
-|------------|---------|---------|
-| **.NET** | 8.0 | Runtime & Framework |
-| **ASP.NET Core** | 8.0 | Web API |
-| **C#** | 12 | Language (Primary Constructors, Record Types) |
-
----
-
-### NuGet Packages
-
-#### API Layer
-
-| Package | Version | Purpose |
-|---------|---------|---------|
-| **Swashbuckle.AspNetCore** | 6.6.2 | Swagger/OpenAPI Documentation |
-| **Serilog.AspNetCore** | 10.0.0 | Strukturiertes Logging |
-| **Serilog.Sinks.File** | 7.0.0 | Log-Datei-Output |
-| **Serilog.Sinks.Seq** | 8.0.0 | Log-Browsing UI (Development) |
-| **Serilog.Enrichers.CorrelationId** | 3.0.1 | Correlation IDs für Request-Tracking |
-| **Asp.Versioning.Http** | 8.1.1 | API Versioning (/api/v1/, /api/v2/) |
-
-#### Application Layer
-
-| Package | Version | Purpose |
-|---------|---------|---------|
-| **MediatR** | 14.1.0 | CQRS Pattern Implementation |
-| **FluentValidation** | 12.1.1 | Input Validation (DTOs) |
-| **FluentValidation.DependencyInjectionExtensions** | 12.1.1 | DI Integration |
-
-#### Infrastructure Layer
-
-| Package | Version | Purpose |
-|---------|---------|---------|
-| **DevExpress.Pdf.Core** | 25.2.8 | PDF-Operationen (Merge, Extract, Sign, etc.) |
-| **Polly** | 8.5.0 | Resilience (Retry, Circuit Breaker, Timeout) - SPÄTER! |
-| **Microsoft.EntityFrameworkCore** | 8.0.0 | ORM für Tenant-Datenbank - SPÄTER! |
-| **Microsoft.EntityFrameworkCore.Sqlite** | 8.0.0 | SQLite Provider - SPÄTER! |
-| **BCrypt.Net-Next** | 4.0.3 | API-Key Hashing - SPÄTER! |
-
-#### Domain Layer
-
-| Package | Version | Purpose |
-|---------|---------|---------|
-| - | - | **Keine Dependencies!** (Clean Architecture) |
-
-#### Tests
-
-| Package | Version | Purpose |
-|---------|---------|---------|
-| **xUnit** | 2.9.3 | Test Framework |
-| **FluentAssertions** | 7.0.0 | Assertions (result.Should().Be(expected)) |
-| **Moq** | 4.20.72 | Mocking (für Services) |
-| **Microsoft.NET.Test.Sdk** | 17.11.1 | Test SDK |
-
----
-
-## ?? PROJECT STRUCTURE
-
-### Solution Overview
-
-```
-DocumentOperator/
-??? DocumentOperator.API/ ? HTTP Entry Point
-??? DocumentOperator.Application/ ? Use Cases (MediatR Handlers)
-??? DocumentOperator.Infrastructure/ ? Technical Implementations
-??? DocumentOperator.Domain/ ? Business Logic (MINIMAL!)
-??? DocumentOperator.Tests/ ? Unit & Integration Tests
-??? ROADMAP.md ? This file
-??? PHASENPLAN.md ? Project timeline
-```
-
----
-
-### ?? API Layer (DocumentOperator.API)
-
-**Purpose:** HTTP Entry Point, Routing, Middleware
-
-**Folder Structure:**
-
-```
-DocumentOperator.API/
-??? Endpoints/
-? ??? v1/
-? ??? DocumentEndpoints.cs ? Minimal API Endpoints
-??? Middleware/
-? ??? ExceptionHandlingMiddleware.cs ? Zentrale Exception Handling ?
-??? Configuration/
-? ??? SwaggerConfiguration.cs ? Swagger Setup
-??? appsettings.json ? Base Configuration
-??? appsettings.Development.json ? Dev Overrides
-??? Program.cs ? Application Entry Point
-```
-
----
-
-### ?? Application Layer (DocumentOperator.Application)
-
-**Purpose:** Use Cases, Business Logic Orchestration
-
-**Folder Structure:**
-
-```
-DocumentOperator.Application/
-??? Features/ ? Vertical Slices ?
-? ??? Documents/
-? ??? ValidatePdf/
-? ? ??? ValidatePdfQuery.cs
-? ? ??? ValidatePdfHandler.cs
-? ? ??? ValidatePdfValidator.cs
-? ??? ExtractAttachments/
-? ? ??? ExtractAttachmentsCommand.cs
-? ? ??? ExtractAttachmentsHandler.cs
-? ? ??? ExtractAttachmentsValidator.cs
-? ??? ... (weitere Features iterativ)
-??? Common/
-? ??? Interfaces/ ? Abstractions für Infrastructure
-? ? ??? IPdfProcessor.cs
-? ??? Behaviors/ ? MediatR Pipeline Behaviors
-? ? ??? ValidationBehavior.cs ? FluentValidation Integration
-? ? ??? LoggingBehavior.cs ? Structured Logging
-? ??? DTOs/ ? Data Transfer Objects
-? ??? ValidatePdfRequest.cs
-? ??? ValidatePdfResponse.cs
-??? DependencyInjection.cs ? Service Registration
-```
-
----
-
-### ?? Infrastructure Layer (DocumentOperator.Infrastructure)
-
-**Purpose:** Technische Implementierungen
-
-**Folder Structure:**
-
-```
-DocumentOperator.Infrastructure/
-??? Services/
-? ??? PdfProcessing/
-? ??? DevExpressPdfProcessor.cs ? IPdfProcessor Implementation ?
-??? Configuration/
-? ??? DocumentOperatorSettings.cs ? Options Pattern Class
-??? DependencyInjection.cs ? Service Registration
-```
-
----
-
-### ?? Domain Layer (DocumentOperator.Domain) - MINIMAL!
-
-**Purpose:** Business Rules (nur was wirklich gebraucht wird!)
-
-**Folder Structure:**
-
-```
-DocumentOperator.Domain/
-??? ValueObjects/ ? Immutable, selbst-validierend ?
-? ??? Base64String.cs
-? ??? TenantId.cs
-? ??? PdfMetadata.cs
-??? Enums/ ? ?
-? ??? DocumentOperationType.cs
-? ??? ProcessingStatus.cs
-??? Exceptions/ ? Domain-spezifische Exceptions ?
- ??? DomainException.cs
- ??? DomainValidationException.cs
- ??? NotFoundException.cs
- ??? PdfProcessingException.cs
-```
-
----
-
-### ?? Tests Layer (DocumentOperator.Tests)
-
-**Purpose:** Unit & Integration Tests
-
-**Folder Structure:**
-
-```
-DocumentOperator.Tests/
-??? Unit/
-? ??? Application/
-? ? ??? Features/
-? ? ??? ValidatePdf/
-? ? ??? ValidatePdfHandlerTests.cs
-? ??? Infrastructure/
-? ? ??? Services/
-? ? ??? DevExpressPdfProcessorTests.cs ?
-? ??? Domain/
-? ??? ValueObjects/
-? ??? Base64StringTests.cs
-??? Integration/
- ??? API/
- ??? DocumentEndpointsTests.cs
-```
-
----
-
-## ?? TESTING STRATEGY
-
-### Test-Driven Development (TDD)
-
-**Flow:**
-1. **Red:** Test schreiben (schlägt fehl, weil Code noch nicht existiert)
-2. **Green:** Code schreiben (Test wird grün)
-3. **Refactor:** Code verbessern (Test bleibt grün)
-
-**Warum TDD?**
-- ? Tests als Dokumentation (wie wird es genutzt?)
-- ? Tests als Safety Net (Refactoring ohne Angst)
-- ? Besseres Design (testbarer Code = guter Code)
-- ? Keine "vergessenen" Tests (Test kommt ZUERST)
-
----
-
-### Test-Pyramide
-
-```
- /\
- / \ E2E Tests (wenige)
- / \
- /------\ Integration Tests (einige)
- / \
- /----------\ Unit Tests (viele)
- / \
-```
-
-**Konkret:**
-- **Unit Tests (viele):**
- - Value Objects (Base64String.Create() wirft Exception?)
- - Handlers (ValidatePdfHandler ruft IPdfProcessor auf?)
- - Services (DevExpressPdfProcessor gibt Metadata zurück?)
-
-- **Integration Tests (einige):**
- - Endpoints (HTTP POST ? 200 OK + JSON?)
- - MediatR Pipeline (ValidationBehavior funktioniert?)
-
-- **E2E Tests (wenige/keine):**
- - Haben wir nicht (API ist selbst der "Top-Level")
-
----
-
-## ?? CURRENT STATUS
-
-### ? Completed
-
-- **Foundation & Domain Layer:**
- - ? Solution Structure (4 Projekte)
- - ? Dependencies (Clean Architecture Dependency Rule)
- - ? Domain Exceptions (4 Exceptions)
- - ? Enums (DocumentOperationType, ProcessingStatus)
- - ? Value Objects (Base64String, TenantId, PdfMetadata)
-
-- **Infrastructure Layer:**
- - ? IPdfProcessor Interface
- - ? DevExpressPdfProcessor.ValidateAsync (mit Tests!)
- - ? DependencyInjection.cs (Infrastructure Services)
-
-- **Application Layer:**
- - ? DependencyInjection.cs (MediatR + FluentValidation)
- - ? ValidationBehavior.cs (FluentValidation Pipeline)
- - ? LoggingBehavior.cs (ILogger Pipeline)
- - ? ValidatePDF Feature (Query, Handler, Validator)
- - ? DTOs (ValidatePdfRequest, ValidatePdfResponse)
- - ? Tests (ValidatePdfHandlerTests - 2 Tests grün)
-
-- **API Layer:**
- - ? ExceptionHandlingMiddleware.cs (RFC 7807 Problem Details)
- - ? DocumentEndpoints.cs (POST /api/v1/documents/validate)
- - ? Program.cs (Services + Middleware + Endpoints)
- - ? Tests (DocumentEndpointsTests - 3 Tests grün)
-
-### ?? In Progress
-
-- **Feature 1: ValidatePDF**
- - ? Step 1.3: Swagger Dokumentation - **NEXT**
-
-### ? Pending
-
-- **Feature 1: ValidatePDF**
- - ? Step 1.3: Swagger Dokumentation
-
-- **Feature 2-5:** ExtractAttachments, ApplyStamp, EmbedCertificate, ConcatenatePDFs
-- **Cross-Cutting Concerns:** Multi-Tenancy, Health Checks, Polly, Logging
-
----
-
-## ?? KEY LEARNINGS & DECISIONS
-
-### 1. Feature-Driven Development statt Layer-by-Layer
-
-**Entscheidung:** Jedes Feature komplett fertig (bis Swagger testbar)
-
-**Warum:**
-- ? Schnellerer Value (Feature 1 nach ~1 Tag fertig!)
-- ? Klares Ziel (Swagger testbar = DONE)
-- ? Weniger Komplexität (nicht alle Layer parallel)
-- ? Besseres Lernen (Pattern wiederholt sich)
-
-**Alternative wäre gewesen:**
-- Domain komplett ? Infrastructure komplett ? Application komplett ? API komplett
-- **Nachteile:** Viel Code ohne sichtbares Ergebnis, spekulativ
-
----
-
-### 2. Domain Layer minimal halten
-
-**Entscheidung:** Nur Enums + Value Objects + Exceptions
-
-**Warum:**
-- Domain = Business-Konzepte (technologie-unabhängig)
-- Service-Anwendung (nicht Domain-lastig)
-- YAGNI (You Ain't Gonna Need It)
-
-**Was bedeutet das?**
-- Domain/ValueObjects/TenantId.cs ? Value Object (immutable, validierend)
-- Infrastructure/Data/Entities/Tenant.cs ? EF Core Entity (später!)
-- Domain kennt KEINE EF Core Dependencies!
-
----
-
-### 3. Multi-Tenancy NACH allen Features
-
-**Entscheidung:** Erst alle synchronen Features, dann Multi-Tenancy
-
-**Warum:**
-- ? Multi-Tenancy betrifft ALLE Endpoints
-- ? Einmal für alle Features (nicht 5x wiederholen!)
-- ? Einfacher zu testen (erst ohne Tenancy, dann mit)
-
-**Nachteile (akzeptiert):**
-- ? Refactoring später nötig (alle Endpoints müssen X-API-Key Header bekommen)
-- ? Aber: Aufwand überschaubar (Middleware erledigt das meiste!)
-
----
-
-### 4. TDD beibehalten
-
-**Entscheidung:** Test-First Development
-
-**Warum:**
-- ? Besseres Design (testbarer Code)
-- ? Tests als Dokumentation
-- ? Safety Net für Refactoring
-
----
-
-### 5. Vertical Slice Architecture
-
-**Entscheidung:** Pro Feature alles zusammen
-
-**Warum:**
-- ? Zusammengehöriger Code ist zusammen
-- ? Einfacher zu finden und zu ändern
-- ? Besser für Teams (weniger Merge-Konflikte)
-
----
-
-## ?? UPDATE LOG
-
-| Date | Feature/Step | Changes |
-|------|--------------|---------|
-| 2024-XX-XX | Foundation | Project setup, dependencies, folder structure |
-| 2024-XX-XX | Domain Layer | Exceptions, Enums, Value Objects |
-| 17.01.2025 | Infrastructure | DevExpressPdfProcessor.ValidateAsync implementiert |
-| 17.01.2025 | Tests | DevExpressPdfProcessorTests.cs erstellt (6 Tests) |
-| 17.01.2025 | **ROADMAP** | ?? **Komplett umstrukturiert** (Feature-Driven Development!) |
-| 17.01.2025 | **PHASENPLAN** | ?? **Komplett umstrukturiert** (Feature-basiert + Datum korrigiert) |
-| 17.01.2025 | **Feature 1 - Step 1.1** | ? **ABGESCHLOSSEN** - Application Layer (MediatR, Behaviors, ValidatePDF Feature, DTOs, Tests) |
-| 17.01.2025 | **Feature 1 - Step 1.2** | ? **ABGESCHLOSSEN** - API Layer (ExceptionMiddleware, Endpoint, Program.cs, Integration Tests - 3/3 grün) |
-| 17.01.2025 | **Feature 1 - Step 1.3** | ? **ABGESCHLOSSEN** - Swagger Dokumentation (SwaggerConfiguration, XML Comments, Endpoint/DTO-Dokumentation - 11/11 Tests grün) |
-| 17.01.2025 | **Feature 1** | ? **KOMPLETT ABGESCHLOSSEN** - ValidatePDF Feature testbar im Swagger UI! |
-| 17.01.2025 | **Fix: Attachment Detection (Multiple Attachments)** | ? **KORRIGIERT** - ValidatePDF erkennt jetzt auch PDFs mit mehreren Attachments korrekt (globale Suche statt 1000-Zeichen-Limit) - 13/13 Tests grün |
-| 17.01.2025 | **Fix: Attachment Count (6 Attachments)** | ? **KORRIGIERT** - AttachmentCount wird jetzt korrekt gezählt (objectCount statt objectCount/2). PDFs mit 6 Attachments werden korrekt erkannt - 13/13 Tests grün |
-| 17.01.2025 | **ROADMAP** | ?? **Feature-Reihenfolge geändert** - Neues Feature 2: ExtractSwissQrCode (Swiss QR Bill Standard 2.0) eingefügt. Alte Features 2-5 werden zu Features 3-6. |
-| 17.01.2025 | **Feature 2 - Step 2.1** | ? **ABGESCHLOSSEN** - Domain Layer (SwissQrCodeData, AddressData Value Objects, SwissQrCodeNotFoundException) |
-| 17.01.2025 | **Feature 2 - Step 2.2** | ? **ABGESCHLOSSEN** - Infrastructure Layer (ISwissQrCodeProcessor, DevExpressSwissQrCodeProcessor, Libraries: Codecrete.SwissQRBill.Generator, ZXing.Net, System.Drawing.Common) |
-| 17.01.2025 | **Feature 2 - Step 2.3** | ? **ABGESCHLOSSEN** - Application Layer (Query, Handler, Validator, DTOs, Unit Tests - 2/2 grün) |
-| 17.01.2025 | **Feature 2 - Step 2.4** | ? **ABGESCHLOSSEN** - API Layer (Endpoint /extract-swiss-qr-code, Exception Handling, Integration Tests - 4/4 grün) |
-| 17.01.2025 | **Feature 2 - Step 2.5** | ? **ABGESCHLOSSEN** - Swagger Dokumentation (XML Comments, Request/Response Beispiele, Endpoint-Beschreibung) |
-| 17.01.2025 | **Feature 2** | ? **KOMPLETT ABGESCHLOSSEN** - ExtractSwissQrCode Feature testbar im Swagger UI! (19/19 Tests grün) |
-
-
----
-
-**END OF ROADMAP**
-
-*This document is a living document and will be updated after each completed step.*