Add ExtractSwissQrCode feature and update feature order
Updated PHASENPLAN.md and ROADMAP.md to reflect the new feature order, making "ExtractSwissQrCode" Feature 2 and renumbering previous Features 2-5 to 3-6. Added detailed steps, endpoints, and acceptance criteria for the new feature. Implemented `ISwissQrCodeProcessor` interface with `DevExpressSwissQrCodeProcessor` for extracting and parsing Swiss QR Codes using DevExpress and Codecrete libraries. Registered the new service in DependencyInjection.cs. Introduced `SwissQrCodeData` value object and `SwissQrCodeNotFoundException` for domain modeling and error handling. Updated project dependencies to include libraries for QR code processing. Adjusted existing feature descriptions and steps to align with the new feature order.
This commit is contained in:
@@ -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) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using DocumentOperator.Domain.ValueObjects;
|
||||
|
||||
namespace DocumentOperator.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for Swiss QR Code processing operations.
|
||||
/// Extracts and parses Swiss QR Codes from PDF documents.
|
||||
/// </summary>
|
||||
public interface ISwissQrCodeProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts and parses Swiss QR Code from the last page of a PDF document.
|
||||
/// </summary>
|
||||
/// <param name="pdfBytes">PDF document as byte array</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Parsed Swiss QR Code data</returns>
|
||||
/// <exception cref="Domain.Exceptions.SwissQrCodeNotFoundException">
|
||||
/// Thrown when no Swiss QR Code is found on the last page
|
||||
/// </exception>
|
||||
/// <exception cref="Domain.Exceptions.PdfProcessingException">
|
||||
/// Thrown when PDF processing fails
|
||||
/// </exception>
|
||||
Task<SwissQrCodeData> ExtractSwissQrCodeAsync(byte[] pdfBytes, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace DocumentOperator.Domain.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a Swiss QR Code cannot be found in a PDF document.
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
129
DocumentOperator.Domain/ValueObjects/SwissQrCodeData.cs
Normal file
129
DocumentOperator.Domain/ValueObjects/SwissQrCodeData.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
namespace DocumentOperator.Domain.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed record SwissQrCodeData
|
||||
{
|
||||
/// <summary>
|
||||
/// QR type - always "SPC" for Swiss Payment Code
|
||||
/// </summary>
|
||||
public required string QrType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the Swiss QR Code standard (e.g., "0200" for version 2.0)
|
||||
/// </summary>
|
||||
public required string Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Character set code (always "1" for UTF-8)
|
||||
/// </summary>
|
||||
public required string CodingType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// IBAN of the creditor (payee)
|
||||
/// </summary>
|
||||
public required string Iban { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Creditor (payee) information
|
||||
/// </summary>
|
||||
public required AddressData Creditor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ultimate creditor information (optional)
|
||||
/// </summary>
|
||||
public AddressData? UltimateCreditor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Payment amount (null if not specified)
|
||||
/// </summary>
|
||||
public decimal? Amount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Currency code (CHF or EUR)
|
||||
/// </summary>
|
||||
public required string Currency { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ultimate debtor (payer) information (optional)
|
||||
/// </summary>
|
||||
public AddressData? UltimateDebtor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference type (QRR, SCOR, or NON)
|
||||
/// </summary>
|
||||
public required string ReferenceType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Payment reference (format depends on ReferenceType)
|
||||
/// </summary>
|
||||
public string? Reference { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unstructured message (max 140 characters)
|
||||
/// </summary>
|
||||
public string? UnstructuredMessage { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Bill information (structured data for automated processing)
|
||||
/// </summary>
|
||||
public string? BillInformation { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Alternative procedure parameters (up to 2 entries)
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? AlternativeProcedureParameters { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents address data in Swiss QR Code (creditor or debtor)
|
||||
/// </summary>
|
||||
public sealed record AddressData
|
||||
{
|
||||
/// <summary>
|
||||
/// Address type: "S" for structured, "K" for combined
|
||||
/// </summary>
|
||||
public required string AddressType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of person or company
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Street name (structured address only)
|
||||
/// </summary>
|
||||
public string? Street { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Building number (structured address only)
|
||||
/// </summary>
|
||||
public string? BuildingNumber { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Address line 1 (combined address only)
|
||||
/// </summary>
|
||||
public string? AddressLine1 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Address line 2 (combined address only)
|
||||
/// </summary>
|
||||
public string? AddressLine2 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Postal code
|
||||
/// </summary>
|
||||
public required string PostalCode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// City/town name
|
||||
/// </summary>
|
||||
public required string City { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Two-letter country code (ISO 3166-1 alpha-2)
|
||||
/// </summary>
|
||||
public required string Country { get; init; }
|
||||
}
|
||||
@@ -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<IPdfProcessor, DevExpressPdfProcessor>();
|
||||
|
||||
// Swiss QR Code Processing Service (DevExpress + Codecrete + ZXing)
|
||||
services.AddScoped<ISwissQrCodeProcessor, DevExpressSwissQrCodeProcessor>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Codecrete.SwissQRBill.Generator" Version="3.4.0" />
|
||||
<PackageReference Include="DevExpress.Document.Processor" Version="26.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="10.0.9" />
|
||||
<PackageReference Include="ZXing.Net.Bindings.Windows.Compatibility" Version="0.16.14" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Swiss QR Code processor using DevExpress PDF API for PDF access
|
||||
/// and Codecrete.SwissQRBill.Generator for QR Code parsing.
|
||||
/// </summary>
|
||||
public sealed class DevExpressSwissQrCodeProcessor : ISwissQrCodeProcessor
|
||||
{
|
||||
private const int QrCodeSearchDpi = 300; // High DPI for better QR code recognition
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<SwissQrCodeData> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a PDF page to a high-resolution bitmap for QR code detection
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes QR code from an image using ZXing library
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses Swiss QR Bill content using Codecrete library
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines reference type based on reference string format
|
||||
/// </summary>
|
||||
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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps Codecrete Address to our AddressData value object
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user