diff --git a/DocumentOperator.API/PHASENPLAN.md b/DocumentOperator.API/PHASENPLAN.md
index 1ba5754..8212c3d 100644
--- a/DocumentOperator.API/PHASENPLAN.md
+++ b/DocumentOperator.API/PHASENPLAN.md
@@ -1,6 +1,6 @@
# ?? DocumentOperator - Phasenplan (Feature-Driven Development)
-> **Stand:** 17.01.2025 | **Aktuell:** Feature 1 - ValidatePDF ? ABGESCHLOSSEN! | **Projektdauer:** 6 Wochen
+> **Stand:** 17.01.2025 | **Aktuell:** Feature 2 - ExtractSwissQrCode ? IN PROGRESS | **Projektdauer:** 6 Wochen
---
@@ -9,10 +9,11 @@
| Woche | Features / Concerns | Status | Fortschritt |
|-------|---------------------|--------|-------------|
| **W1** | Feature 1: ValidatePDF | ? Abgeschlossen | 100% (Foundation + Application + API + Swagger fertig) |
-| **W2** | Feature 2: ExtractAttachments | ? Geplant | 0% |
-| **W2** | Feature 3: ApplyStamp | ? Geplant | 0% |
-| **W3** | Feature 4: EmbedCertificate | ? Geplant | 0% |
-| **W3** | Feature 5: ConcatenatePDFs (Async) | ? Geplant | 0% |
+| **W1-W2** | Feature 2: ExtractSwissQrCode | ? In Progress | 0% (Gerade gestartet!) |
+| **W2** | Feature 3: ExtractAttachments | ? Geplant | 0% |
+| **W2** | Feature 4: ApplyStamp | ? Geplant | 0% |
+| **W3** | Feature 5: EmbedCertificate | ? Geplant | 0% |
+| **W3** | Feature 6: ConcatenatePDFs (Async) | ? Geplant | 0% |
| **W4** | Multi-Tenancy (X-API-Key Header) | ? Geplant | 0% |
| **W5** | Health Checks + Polly + Logging | ? Geplant | 0% |
| **W6** | Production Deployment | ? Geplant | 0% |
@@ -169,19 +170,52 @@
---
-### WOCHE 2 - Features 2 & 3 | ? Geplant - 0%
+### WOCHE 1-2 - Feature 2: ExtractSwissQrCode | ? In Progress - 0%
-#### Feature 2: ExtractAttachments (Synchron)
+**Dauer:** ~1-2 Tage
+**Status:** ? In Progress (gerade gestartet!)
+
+**Endpoint:** POST /api/v1/documents/extract-swiss-qr-code
+
+**Was wird gebaut:**
+- Client sendet Referenzen (Array) + PDF (Base64)
+- API extrahiert Swiss QR Code von **letzter Seite**
+- API gibt Referenzen + alle QR Code Felder zurück (Swiss QR Bill Standard 2.0)
+
+**Technologie:**
+- **DevExpress PDF Document API** (PDF-Zugriff, letzte Seite)
+- **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 + Codecrete Integration)
+- ?? 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
+
+---
+
+### WOCHE 2 - Features 3 & 4 | ? Geplant - 0%
+
+#### Feature 3: ExtractAttachments (Synchron)
**Dauer:** ~1 Tag
**Status:** ? Pending
**Endpoint:** POST /api/v1/documents/extract-attachments
**Steps:**
-- ? Step 2.1: Infrastructure Layer (DevExpressPdfProcessor.ExtractAttachmentsAsync)
-- ? Step 2.2: Application Layer (ExtractAttachmentsCommand + Handler + Validator)
-- ? Step 2.3: API Layer (Endpoint)
-- ? Step 2.4: Swagger Dokumentation
+- ? 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
**Akzeptanzkriterien:**
- ? Endpoint im Swagger testbar
@@ -189,17 +223,17 @@
---
-#### Feature 3: ApplyStamp (Synchron)
+#### Feature 4: ApplyStamp (Synchron)
**Dauer:** ~1 Tag
**Status:** ? Pending
**Endpoint:** POST /api/v1/documents/apply-stamp
**Steps:**
-- ? Step 3.1: Infrastructure Layer (DevExpressPdfProcessor.ApplyStampAsync)
-- ? Step 3.2: Application Layer (ApplyStampCommand + Handler + Validator)
-- ? Step 3.3: API Layer (Endpoint)
-- ? Step 3.4: Swagger Dokumentation
+- ? 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
**Akzeptanzkriterien:**
- ? Endpoint im Swagger testbar
@@ -208,19 +242,19 @@
---
-### WOCHE 3 - Features 4 & 5 | ? Geplant - 0%
+### WOCHE 3 - Features 5 & 6 | ? Geplant - 0%
-#### Feature 4: EmbedCertificate (Synchron)
+#### Feature 5: EmbedCertificate (Synchron)
**Dauer:** ~1 Tag
**Status:** ? Pending
**Endpoint:** POST /api/v1/documents/embed-certificate
**Steps:**
-- ? Step 4.1: Infrastructure Layer (DevExpressPdfProcessor.EmbedCertificateAsync)
-- ? Step 4.2: Application Layer (EmbedCertificateCommand + Handler + Validator)
-- ? Step 4.3: API Layer (Endpoint)
-- ? Step 4.4: Swagger Dokumentation
+- ? 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
**Akzeptanzkriterien:**
- ? Endpoint im Swagger testbar
@@ -229,7 +263,7 @@
---
-#### Feature 5: ConcatenatePDFs (Asynchron)
+#### Feature 6: ConcatenatePDFs (Asynchron)
**Dauer:** ~2 Tage
**Status:** ? Pending
@@ -239,10 +273,10 @@
- GET /api/v1/jobs/{jobId}/download (Ergebnis herunterladen)
**Steps:**
-- ? Step 5.1: Infrastructure Layer (In-Memory Queue + Background Worker)
-- ? Step 5.2: Application Layer (SubmitConcatenateJobCommand + GetJobStatusQuery)
-- ? Step 5.3: API Layer (Async Endpoints)
-- ? Step 5.4: Swagger Dokumentation
+- ? 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
**Akzeptanzkriterien:**
- ? POST /concatenate gibt JobId zurück
@@ -453,16 +487,21 @@
### Nächstes Feature
-**Feature 2: ExtractAttachments** - **NEXT**
-1. ? Step 2.1: Infrastructure Layer (DevExpressPdfProcessor.ExtractAttachmentsAsync)
-2. ? Step 2.2: Application Layer (ExtractAttachmentsCommand + Handler + Validator + DTOs)
-3. ? Step 2.3: API Layer (Endpoint + Integration Tests)
-4. ? Step 2.4: Swagger Dokumentation
+**Feature 2: ExtractSwissQrCode** - **IN PROGRESS**
+1. ?? Step 2.1: Domain Layer (SwissQrCodeData Value Object)
+2. ?? Step 2.2: Infrastructure Layer (IQrCodeProcessor + DevExpressSwissQrCodeProcessor + Codecrete Integration)
+3. ?? Step 2.3: Application Layer (ExtractSwissQrCodeQuery + Handler + Validator + DTOs)
+4. ?? Step 2.4: API Layer (Endpoint + Integration Tests)
+5. ?? Step 2.5: Swagger Dokumentation
-**Erwarteter Zeitaufwand:** ~1 Tag
+**Erwarteter Zeitaufwand:** ~1-2 Tage
**Akzeptanzkriterien:**
-- ? POST /api/v1/documents/extract-attachments im Swagger testbar
+- ? POST /api/v1/documents/extract-swiss-qr-code im Swagger testbar
+- ? Swiss QR Code wird von letzter Seite extrahiert
+- ? Alle Swiss QR Bill Felder geparst (Standard 2.0)
+- ? Referenzen durchgeschliffen
+- ? Fehler wenn kein QR Code gefunden
- ? Alle Tests grün
- ? Clean Architecture eingehalten
@@ -483,6 +522,8 @@
| 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 | **PHASENPLAN** | ?? **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 - ExtractSwissQrCode** | ?? **GESTARTET** - Swiss QR Code Extraktion von letzter PDF-Seite (DevExpress + Codecrete.SwissQRBill.Generator) |
---
diff --git a/DocumentOperator.API/ROADMAP.md b/DocumentOperator.API/ROADMAP.md
index a0df384..2cffde6 100644
--- a/DocumentOperator.API/ROADMAP.md
+++ b/DocumentOperator.API/ROADMAP.md
@@ -45,10 +45,11 @@
| Feature | Type | Status | Swagger Testbar? |
|---------|------|--------|------------------|
| **1. ValidatePDF** | Synchron | ? Abgeschlossen | ? |
-| **2. ExtractAttachments** | Synchron | ? Pending | ? |
-| **3. ApplyStamp** | Synchron | ? Pending | ? |
-| **4. EmbedCertificate** | Synchron | ? Pending | ? |
-| **5. ConcatenatePDFs** | Asynchron | ? Pending | ? |
+| **2. ExtractSwissQrCode** | Synchron | ? In Progress | ? |
+| **3. ExtractAttachments** | Synchron | ? Pending | ? |
+| **4. ApplyStamp** | Synchron | ? Pending | ? |
+| **5. EmbedCertificate** | Synchron | ? Pending | ? |
+| **6. ConcatenatePDFs** | Asynchron | ? Pending | ? |
### ?? Cross-Cutting Concerns (nach Features 1-4)
@@ -278,11 +279,85 @@ Response: { "pageCount": 5, "fileSizeBytes": 1024, "pdfVersion": "1.4", "hasAtta
- ? Swagger-Dokumentation vollständig
**Nächstes Feature:**
-? **Feature 2: ExtractAttachments**
+? **Feature 2: ExtractSwissQrCode**
---
-## ?? FEATURE 2: ExtractAttachments (Synchron) - **NEXT**
+## ?? FEATURE 2: ExtractSwissQrCode (Synchron) - **IN PROGRESS**
+
+**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
+
+---
+
+## ?? FEATURE 3: ExtractAttachments (Synchron) - **PENDING**
**Was macht dieses Feature?**
- Client sendet PDF als Base64 (JSON)
@@ -297,14 +372,14 @@ Response: { "attachments": [{ "name": "invoice.xml", "base64Content": "..." }] }
```
**Steps:**
-- ?? Step 2.1: Infrastructure Layer (DevExpressPdfProcessor.ExtractAttachmentsAsync)
-- ?? Step 2.2: Application Layer (ExtractAttachmentsCommand + Handler + Validator)
-- ?? Step 2.3: API Layer (Endpoint)
-- ?? Step 2.4: Swagger Dokumentation
+- ?? 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 3: ApplyStamp (Synchron) - **PENDING**
+## ?? FEATURE 4: ApplyStamp (Synchron) - **PENDING**
**Was macht dieses Feature?**
- Client sendet PDF + Stamp-Konfiguration (Text, Position)
@@ -319,14 +394,14 @@ Response: { "base64Pdf": "JVBERi0xLjQK..." }
```
**Steps:**
-- ?? Step 3.1: Infrastructure Layer (DevExpressPdfProcessor.ApplyStampAsync)
-- ?? Step 3.2: Application Layer (ApplyStampCommand + Handler + Validator)
-- ?? Step 3.3: API Layer (Endpoint)
-- ?? Step 3.4: Swagger Dokumentation
+- ?? 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 4: EmbedCertificate (Synchron) - **PENDING**
+## ?? FEATURE 5: EmbedCertificate (Synchron) - **PENDING**
**Was macht dieses Feature?**
- Client sendet PDF + Zertifikat (PFX als Base64)
@@ -341,14 +416,14 @@ Response: { "base64Pdf": "JVBERi0xLjQK..." }
```
**Steps:**
-- ?? Step 4.1: Infrastructure Layer (DevExpressPdfProcessor.EmbedCertificateAsync)
-- ?? Step 4.2: Application Layer (EmbedCertificateCommand + Handler + Validator)
-- ?? Step 4.3: API Layer (Endpoint)
-- ?? Step 4.4: Swagger Dokumentation
+- ?? 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 5: ConcatenatePDFs (Asynchron) - **PENDING**
+## ?? FEATURE 6: ConcatenatePDFs (Asynchron) - **PENDING**
**Was macht dieses Feature?**
- Client sendet mehrere PDFs (Array von Base64)
@@ -370,10 +445,10 @@ Response: PDF-Datei (Binary)
```
**Steps:**
-- ?? Step 5.1: Infrastructure Layer (In-Memory Queue + Background Worker)
-- ?? Step 5.2: Application Layer (SubmitConcatenateJobCommand + GetJobStatusQuery)
-- ?? Step 5.3: API Layer (Async Endpoints)
-- ?? Step 5.4: Swagger Dokumentation
+- ?? 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
---
@@ -995,6 +1070,8 @@ DocumentOperator.Tests/
| 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 - ExtractSwissQrCode** | ?? **GESTARTET** - Swiss QR Code Extraktion von letzter PDF-Seite (DevExpress + Codecrete.SwissQRBill.Generator) |
---
diff --git a/DocumentOperator.Application/Common/Interfaces/ISwissQrCodeProcessor.cs b/DocumentOperator.Application/Common/Interfaces/ISwissQrCodeProcessor.cs
new file mode 100644
index 0000000..c832fe8
--- /dev/null
+++ b/DocumentOperator.Application/Common/Interfaces/ISwissQrCodeProcessor.cs
@@ -0,0 +1,24 @@
+using DocumentOperator.Domain.ValueObjects;
+
+namespace DocumentOperator.Application.Common.Interfaces;
+
+///
+/// Interface for Swiss QR Code processing operations.
+/// Extracts and parses Swiss QR Codes from PDF documents.
+///
+public interface ISwissQrCodeProcessor
+{
+ ///
+ /// Extracts and parses Swiss QR Code from the last page of a PDF document.
+ ///
+ /// PDF document as byte array
+ /// Cancellation token
+ /// Parsed Swiss QR Code data
+ ///
+ /// Thrown when no Swiss QR Code is found on the last page
+ ///
+ ///
+ /// Thrown when PDF processing fails
+ ///
+ Task ExtractSwissQrCodeAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
+}
diff --git a/DocumentOperator.Domain/Exceptions/SwissQrCodeNotFoundException.cs b/DocumentOperator.Domain/Exceptions/SwissQrCodeNotFoundException.cs
new file mode 100644
index 0000000..b06eb8a
--- /dev/null
+++ b/DocumentOperator.Domain/Exceptions/SwissQrCodeNotFoundException.cs
@@ -0,0 +1,22 @@
+namespace DocumentOperator.Domain.Exceptions;
+
+///
+/// Exception thrown when a Swiss QR Code cannot be found in a PDF document.
+///
+public sealed class SwissQrCodeNotFoundException : Exception
+{
+ public SwissQrCodeNotFoundException()
+ : base("No Swiss QR Code found on the last page of the PDF document.")
+ {
+ }
+
+ public SwissQrCodeNotFoundException(string message)
+ : base(message)
+ {
+ }
+
+ public SwissQrCodeNotFoundException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
diff --git a/DocumentOperator.Domain/ValueObjects/SwissQrCodeData.cs b/DocumentOperator.Domain/ValueObjects/SwissQrCodeData.cs
new file mode 100644
index 0000000..f61348e
--- /dev/null
+++ b/DocumentOperator.Domain/ValueObjects/SwissQrCodeData.cs
@@ -0,0 +1,129 @@
+namespace DocumentOperator.Domain.ValueObjects;
+
+///
+/// Represents Swiss QR Code data according to Swiss QR Bill Standard 2.0.
+/// Immutable value object containing all fields from a Swiss QR payment part.
+///
+public sealed record SwissQrCodeData
+{
+ ///
+ /// QR type - always "SPC" for Swiss Payment Code
+ ///
+ public required string QrType { get; init; }
+
+ ///
+ /// Version of the Swiss QR Code standard (e.g., "0200" for version 2.0)
+ ///
+ public required string Version { get; init; }
+
+ ///
+ /// Character set code (always "1" for UTF-8)
+ ///
+ public required string CodingType { get; init; }
+
+ ///
+ /// IBAN of the creditor (payee)
+ ///
+ public required string Iban { get; init; }
+
+ ///
+ /// Creditor (payee) information
+ ///
+ public required AddressData Creditor { get; init; }
+
+ ///
+ /// Ultimate creditor information (optional)
+ ///
+ public AddressData? UltimateCreditor { get; init; }
+
+ ///
+ /// Payment amount (null if not specified)
+ ///
+ public decimal? Amount { get; init; }
+
+ ///
+ /// Currency code (CHF or EUR)
+ ///
+ public required string Currency { get; init; }
+
+ ///
+ /// Ultimate debtor (payer) information (optional)
+ ///
+ public AddressData? UltimateDebtor { get; init; }
+
+ ///
+ /// Reference type (QRR, SCOR, or NON)
+ ///
+ public required string ReferenceType { get; init; }
+
+ ///
+ /// Payment reference (format depends on ReferenceType)
+ ///
+ public string? Reference { get; init; }
+
+ ///
+ /// Unstructured message (max 140 characters)
+ ///
+ public string? UnstructuredMessage { get; init; }
+
+ ///
+ /// Bill information (structured data for automated processing)
+ ///
+ public string? BillInformation { get; init; }
+
+ ///
+ /// Alternative procedure parameters (up to 2 entries)
+ ///
+ public IReadOnlyList? AlternativeProcedureParameters { get; init; }
+}
+
+///
+/// Represents address data in Swiss QR Code (creditor or debtor)
+///
+public sealed record AddressData
+{
+ ///
+ /// Address type: "S" for structured, "K" for combined
+ ///
+ public required string AddressType { get; init; }
+
+ ///
+ /// Name of person or company
+ ///
+ public required string Name { get; init; }
+
+ ///
+ /// Street name (structured address only)
+ ///
+ public string? Street { get; init; }
+
+ ///
+ /// Building number (structured address only)
+ ///
+ public string? BuildingNumber { get; init; }
+
+ ///
+ /// Address line 1 (combined address only)
+ ///
+ public string? AddressLine1 { get; init; }
+
+ ///
+ /// Address line 2 (combined address only)
+ ///
+ public string? AddressLine2 { get; init; }
+
+ ///
+ /// Postal code
+ ///
+ public required string PostalCode { get; init; }
+
+ ///
+ /// City/town name
+ ///
+ public required string City { get; init; }
+
+ ///
+ /// Two-letter country code (ISO 3166-1 alpha-2)
+ ///
+ public required string Country { get; init; }
+}
diff --git a/DocumentOperator.Infrastructure/DependencyInjection.cs b/DocumentOperator.Infrastructure/DependencyInjection.cs
index 1bab6a8..e48bb13 100644
--- a/DocumentOperator.Infrastructure/DependencyInjection.cs
+++ b/DocumentOperator.Infrastructure/DependencyInjection.cs
@@ -1,5 +1,6 @@
using DocumentOperator.Application.Common.Interfaces;
using DocumentOperator.Infrastructure.Services.PdfProcessing;
+using DocumentOperator.Infrastructure.Services.QrCodeProcessing;
using Microsoft.Extensions.DependencyInjection;
namespace DocumentOperator.Infrastructure;
@@ -17,6 +18,9 @@ public static class DependencyInjection
// PDF Processing Service (DevExpress)
services.AddScoped();
+ // Swiss QR Code Processing Service (DevExpress + Codecrete + ZXing)
+ services.AddScoped();
+
return services;
}
}
diff --git a/DocumentOperator.Infrastructure/DocumentOperator.Infrastructure.csproj b/DocumentOperator.Infrastructure/DocumentOperator.Infrastructure.csproj
index a6c9ebc..19a62ac 100644
--- a/DocumentOperator.Infrastructure/DocumentOperator.Infrastructure.csproj
+++ b/DocumentOperator.Infrastructure/DocumentOperator.Infrastructure.csproj
@@ -7,8 +7,11 @@
+
+
+
diff --git a/DocumentOperator.Infrastructure/Services/QrCodeProcessing/DevExpressSwissQrCodeProcessor.cs b/DocumentOperator.Infrastructure/Services/QrCodeProcessing/DevExpressSwissQrCodeProcessor.cs
new file mode 100644
index 0000000..e767283
--- /dev/null
+++ b/DocumentOperator.Infrastructure/Services/QrCodeProcessing/DevExpressSwissQrCodeProcessor.cs
@@ -0,0 +1,175 @@
+using Codecrete.SwissQRBill.Generator;
+using DevExpress.Pdf;
+using DocumentOperator.Application.Common.Interfaces;
+using DocumentOperator.Domain.Exceptions;
+using DocumentOperator.Domain.ValueObjects;
+using System.Drawing;
+using ZXing;
+
+namespace DocumentOperator.Infrastructure.Services.QrCodeProcessing;
+
+///
+/// Swiss QR Code processor using DevExpress PDF API for PDF access
+/// and Codecrete.SwissQRBill.Generator for QR Code parsing.
+///
+public sealed class DevExpressSwissQrCodeProcessor : ISwissQrCodeProcessor
+{
+ private const int QrCodeSearchDpi = 300; // High DPI for better QR code recognition
+
+ ///
+ public async Task ExtractSwissQrCodeAsync(byte[] pdfBytes, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(pdfBytes);
+
+ try
+ {
+ using var pdfDocument = new PdfDocumentProcessor();
+ pdfDocument.LoadDocument(new MemoryStream(pdfBytes));
+
+ if (pdfDocument.Document.Pages.Count == 0)
+ {
+ throw new ArgumentException("PDF document contains no pages.", nameof(pdfBytes));
+ }
+
+ // Get last page
+ int lastPageIndex = pdfDocument.Document.Pages.Count - 1;
+
+ // Convert last page to image for QR code detection
+ using var pageImage = RenderPageToImage(pdfDocument, lastPageIndex);
+
+ // Detect and decode QR code
+ string? qrCodeContent = DecodeQrCodeFromImage(pageImage);
+
+ if (string.IsNullOrEmpty(qrCodeContent))
+ {
+ throw new SwissQrCodeNotFoundException(
+ $"No QR Code found on the last page (page {lastPageIndex + 1}) of the PDF document.");
+ }
+
+ // Parse Swiss QR Bill content using Codecrete library
+ SwissQrCodeData qrCodeData = ParseSwissQrBillContent(qrCodeContent);
+
+ return await Task.FromResult(qrCodeData);
+ }
+ catch (SwissQrCodeNotFoundException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw new ArgumentException("Failed to extract Swiss QR Code from PDF.", nameof(pdfBytes), ex);
+ }
+ }
+
+ ///
+ /// Renders a PDF page to a high-resolution bitmap for QR code detection
+ ///
+ private static Bitmap RenderPageToImage(PdfDocumentProcessor processor, int pageIndex)
+ {
+ // Render page at high DPI for better QR code recognition
+ var pageImage = processor.CreateBitmap(pageIndex + 1, QrCodeSearchDpi);
+ return pageImage;
+ }
+
+ ///
+ /// Decodes QR code from an image using ZXing library
+ ///
+ private static string? DecodeQrCodeFromImage(Bitmap image)
+ {
+ var reader = new ZXing.Windows.Compatibility.BarcodeReader
+ {
+ AutoRotate = true,
+ TryInverted = true,
+ Options = new ZXing.Common.DecodingOptions
+ {
+ PossibleFormats = new[] { BarcodeFormat.QR_CODE },
+ TryHarder = true
+ }
+ };
+
+ var result = reader.Decode(image);
+ return result?.Text;
+ }
+
+ ///
+ /// Parses Swiss QR Bill content using Codecrete library
+ ///
+ private static SwissQrCodeData ParseSwissQrBillContent(string qrCodeText)
+ {
+ try
+ {
+ // Decode Swiss QR Bill using Codecrete library
+ var bill = QRBill.DecodeQrCodeText(qrCodeText);
+
+ // Determine reference type based on presence and format of reference
+ string referenceType = DetermineReferenceType(bill.Reference);
+
+ // Map AlternativeSchemes to string array
+ var alternativeParams = bill.AlternativeSchemes?
+ .Select(s => $"{s.Name}: {s.Instruction}")
+ .ToArray();
+
+ // Map to our domain value object
+ return new SwissQrCodeData
+ {
+ QrType = "SPC", // Always SPC for Swiss Payment Code
+ Version = bill.Version.ToString("D4"), // e.g., "0200" for version 2.0
+ CodingType = "1", // Always UTF-8
+ Iban = bill.Account ?? string.Empty,
+ Creditor = MapAddress(bill.Creditor),
+ UltimateCreditor = null, // Not exposed in Codecrete Bill model
+ Amount = bill.Amount,
+ Currency = bill.Currency ?? "CHF",
+ UltimateDebtor = bill.Debtor != null ? MapAddress(bill.Debtor) : null,
+ ReferenceType = referenceType,
+ Reference = bill.Reference,
+ UnstructuredMessage = bill.UnstructuredMessage,
+ BillInformation = bill.BillInformation,
+ AlternativeProcedureParameters = alternativeParams
+ };
+ }
+ catch (Exception ex)
+ {
+ throw new ArgumentException(
+ "Failed to parse Swiss QR Code content. The QR code may not be a valid Swiss QR Bill.", "qrCodeText", ex);
+ }
+ }
+
+ ///
+ /// Determines reference type based on reference string format
+ ///
+ private static string DetermineReferenceType(string? reference)
+ {
+ if (string.IsNullOrWhiteSpace(reference))
+ return "NON";
+
+ // QRR (QR Reference): 27 digits
+ if (reference.Length == 27 && reference.All(char.IsDigit))
+ return "QRR";
+
+ // SCOR (Creditor Reference ISO 11649): starts with RF and has check digits
+ if (reference.StartsWith("RF", StringComparison.OrdinalIgnoreCase) && reference.Length >= 5)
+ return "SCOR";
+
+ return "NON";
+ }
+
+ ///
+ /// Maps Codecrete Address to our AddressData value object
+ ///
+ private static AddressData MapAddress(Codecrete.SwissQRBill.Generator.Address address)
+ {
+ return new AddressData
+ {
+ AddressType = address.Type == Codecrete.SwissQRBill.Generator.Address.AddressType.Structured ? "S" : "K",
+ Name = address.Name ?? string.Empty,
+ Street = address.Street,
+ BuildingNumber = address.HouseNo,
+ AddressLine1 = address.AddressLine1,
+ AddressLine2 = address.AddressLine2,
+ PostalCode = address.PostalCode ?? string.Empty,
+ City = address.Town ?? string.Empty,
+ Country = address.CountryCode ?? string.Empty
+ };
+ }
+}