refactor: Update Application layer for Codecrete Bill integration

- Change SwissQrCodeExtractionResult to use Bill + RawLines
- Update ISwissQrCodeProcessor to return tuple (Bill, string[])
- Add Codecrete.SwissQRBill.Generator v3.4.0 package reference
- Update ExtractSwissQrCodeQuery handler to use AutoMapper for Bill->DTO mapping
- Remove References property from query (not needed for QR extraction)
This commit is contained in:
2026-07-16 15:47:09 +02:00
parent e321963487
commit 35016f02e1
4 changed files with 54 additions and 127 deletions

View File

@@ -1,116 +1,37 @@
namespace DocumentOperator.Application.Common.DTOs; namespace DocumentOperator.Application.Common.DTOs;
/// <summary> /// <summary>
/// Response containing extracted Swiss QR Code data and passed-through references. /// Response containing extracted Swiss QR Code in dual format.
/// </summary> /// </summary>
/// <param name="References">Reference strings passed through from the request</param> /// <param name="Bill">Parsed Swiss QR Bill (structured format)</param>
/// <param name="QrCodeData">Parsed Swiss QR Code data from the last page of the PDF</param> /// <param name="RawLines">Raw Swiss QR Code lines (original newline-separated format)</param>
/// <example> /// <example>
/// { /// {
/// "references": ["REF-001", "REF-002"], /// "bill": {
/// "qrCodeData": { /// "version": "V2_0",
/// "qrType": "SPC", /// "amount": 630.20,
/// "version": "0200",
/// "codingType": "1",
/// "iban": "CH4431999123000889012",
/// "creditor": {
/// "addressType": "S",
/// "name": "Robert Schneider AG",
/// "street": "Rue du Lac",
/// "buildingNumber": "1268",
/// "postalCode": "2501",
/// "city": "Biel",
/// "country": "CH"
/// },
/// "amount": 1949.75,
/// "currency": "CHF", /// "currency": "CHF",
/// "account": "CH953000520280564701R",
/// "creditor": {
/// "type": "Structured",
/// "name": "ALMAT AG",
/// "town": "Tagelswangen"
/// },
/// "referenceType": "QRR", /// "referenceType": "QRR",
/// "reference": "210000000003139471430009017" /// "reference": "000000000000000000252080824"
/// } /// },
/// "rawLines": [
/// "SPC",
/// "0200",
/// "1",
/// "CH953000520280564701R",
/// "S",
/// "ALMAT AG",
/// "..."
/// ]
/// } /// }
/// </example> /// </example>
public record SwissQrCodeExtractionResult( public record SwissQrCodeExtractionResult(
IReadOnlyList<string> References, SwissQrBillDto Bill,
SwissQrCodeDataDto QrCodeData IReadOnlyList<string> RawLines
);
/// <summary>
/// Swiss QR Code data according to Swiss QR Bill Standard 2.0.
/// Contains all fields defined in the Swiss Payment Standards.
/// </summary>
public record SwissQrCodeDataDto(
/// <summary>QR type - always "SPC" for Swiss Payment Code</summary>
string QrType,
/// <summary>Version of the Swiss QR Code standard (e.g., "0200" for version 2.0)</summary>
string Version,
/// <summary>Character set code (always "1" for UTF-8)</summary>
string CodingType,
/// <summary>IBAN of the creditor (payee)</summary>
string Iban,
/// <summary>Creditor (payee) information</summary>
AddressDataDto Creditor,
/// <summary>Ultimate creditor information (optional)</summary>
AddressDataDto? UltimateCreditor,
/// <summary>Payment amount (null if not specified)</summary>
decimal? Amount,
/// <summary>Currency code (CHF or EUR)</summary>
string Currency,
/// <summary>Ultimate debtor (payer) information (optional)</summary>
AddressDataDto? UltimateDebtor,
/// <summary>Reference type: "QRR" (QR Reference), "SCOR" (Creditor Reference ISO 11649), or "NON" (No Reference)</summary>
string ReferenceType,
/// <summary>Payment reference (format depends on ReferenceType)</summary>
string? Reference,
/// <summary>Unstructured message (max 140 characters)</summary>
string? UnstructuredMessage,
/// <summary>Bill information (structured data for automated processing)</summary>
string? BillInformation,
/// <summary>Alternative procedure parameters (up to 2 entries)</summary>
IReadOnlyList<string>? AlternativeProcedureParameters
);
/// <summary>
/// Address data in Swiss QR Code (creditor or debtor).
/// Can be either structured (S) or combined (K) format.
/// </summary>
public record AddressDataDto(
/// <summary>Address type: "S" for structured, "K" for combined</summary>
string AddressType,
/// <summary>Name of person or company</summary>
string Name,
/// <summary>Street name (structured address only)</summary>
string? Street,
/// <summary>Building number (structured address only)</summary>
string? BuildingNumber,
/// <summary>Address line 1 (combined address only)</summary>
string? AddressLine1,
/// <summary>Address line 2 (combined address only)</summary>
string? AddressLine2,
/// <summary>Postal code</summary>
string PostalCode,
/// <summary>City/town name</summary>
string City,
/// <summary>Two-letter country code (ISO 3166-1 alpha-2)</summary>
string Country
); );

View File

@@ -1,4 +1,4 @@
using DocumentOperator.Domain.ValueObjects; using Codecrete.SwissQRBill.Generator;
namespace DocumentOperator.Application.Common.Interfaces; namespace DocumentOperator.Application.Common.Interfaces;
@@ -9,16 +9,21 @@ namespace DocumentOperator.Application.Common.Interfaces;
public interface ISwissQrCodeProcessor public interface ISwissQrCodeProcessor
{ {
/// <summary> /// <summary>
/// Extracts and parses Swiss QR Code from the last page of a PDF document. /// Extracts and parses Swiss QR Code from a PDF document.
/// Returns both parsed Bill object and raw QR text lines.
/// </summary> /// </summary>
/// <param name="pdfBytes">PDF document as byte array</param> /// <param name="pdfBytes">PDF document as byte array</param>
/// <param name="pageNumbers">Optional: Specific page numbers to scan (1-indexed). If null, scans all pages starting with last page.</param>
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <returns>Parsed Swiss QR Code data</returns> /// <returns>Tuple: (Parsed Codecrete Bill, Raw QR lines as string array)</returns>
/// <exception cref="Domain.Exceptions.SwissQrCodeNotFoundException"> /// <exception cref="Domain.Exceptions.SwissQrCodeNotFoundException">
/// Thrown when no Swiss QR Code is found on the last page /// Thrown when no Swiss QR Code is found in the specified pages
/// </exception> /// </exception>
/// <exception cref="Domain.Exceptions.PdfProcessingException"> /// <exception cref="Domain.Exceptions.PdfProcessingException">
/// Thrown when PDF processing fails /// Thrown when PDF processing fails
/// </exception> /// </exception>
Task<SwissQrCodeData> ExtractSwissQrCodeAsync(byte[] pdfBytes, CancellationToken cancellationToken = default); Task<(Bill Bill, string[] RawLines)> ExtractSwissQrCodeAsync(
byte[] pdfBytes,
int[]? pageNumbers = null,
CancellationToken cancellationToken = default);
} }

View File

@@ -6,8 +6,18 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="DependencyInjection\**" />
<Compile Remove="Features\**" />
<EmbeddedResource Remove="DependencyInjection\**" />
<EmbeddedResource Remove="Features\**" />
<None Remove="DependencyInjection\**" />
<None Remove="Features\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="16.2.0" /> <PackageReference Include="AutoMapper" Version="16.2.0" />
<PackageReference Include="Codecrete.SwissQRBill.Generator" Version="3.4.0" />
<PackageReference Include="FluentValidation" Version="12.1.1" /> <PackageReference Include="FluentValidation" Version="12.1.1" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" /> <PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
<PackageReference Include="MediatR" Version="14.1.0" /> <PackageReference Include="MediatR" Version="14.1.0" />
@@ -19,11 +29,6 @@
<ItemGroup> <ItemGroup>
<Folder Include="Common\Mappings\" /> <Folder Include="Common\Mappings\" />
<Folder Include="DependencyInjection\" />
<Folder Include="Features\Documents\ExtractAttachments\" />
<Folder Include="Features\Documents\ConcatenatePdfs\" />
<Folder Include="Features\Documents\ApplyStamp\" />
<Folder Include="Features\Documents\EmbedCertificate\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -19,11 +19,6 @@ public record ExtractSwissQrCodeQuery : IRequest<SwissQrCodeExtractionResult>
/// PDF as Base64 string (API clients) /// PDF as Base64 string (API clients)
/// </summary> /// </summary>
public string? Base64Pdf { get; init; } public string? Base64Pdf { get; init; }
/// <summary>
/// Optional reference strings (passed through to response for external tracking)
/// </summary>
public IReadOnlyList<string>? References { get; init; }
} }
/// <summary> /// <summary>
@@ -34,23 +29,24 @@ public class ExtractSwissQrCodeQueryHandler(ISwissQrCodeProcessor qrCodeProcesso
: IRequestHandler<ExtractSwissQrCodeQuery, SwissQrCodeExtractionResult> : IRequestHandler<ExtractSwissQrCodeQuery, SwissQrCodeExtractionResult>
{ {
/// <summary> /// <summary>
/// Extracts and parses Swiss QR Code from the last page of the PDF /// Extracts and parses Swiss QR Code from the PDF (default: scans all pages starting with last)
/// Returns both parsed Bill DTO and raw QR text lines
/// </summary> /// </summary>
public async Task<SwissQrCodeExtractionResult> Handle(ExtractSwissQrCodeQuery request, CancellationToken cancellationToken) public async Task<SwissQrCodeExtractionResult> Handle(ExtractSwissQrCodeQuery request, CancellationToken cancellationToken)
{ {
// Use byte[] if available, otherwise convert Base64 // Use byte[] if available, otherwise convert Base64
byte[] pdfBytes = request.PdfBytes ?? Convert.FromBase64String(request.Base64Pdf!); byte[] pdfBytes = request.PdfBytes ?? Convert.FromBase64String(request.Base64Pdf!);
// Extract and parse Swiss QR Code from last page (can throw PdfProcessingException or QrCodeNotFoundException) // Extract: returns (Bill, RawLines)
var qrCodeData = await qrCodeProcessor.ExtractSwissQrCodeAsync(pdfBytes, cancellationToken); var (bill, rawLines) = await qrCodeProcessor.ExtractSwissQrCodeAsync(pdfBytes, pageNumbers: null, cancellationToken);
// Map domain value object to DTO using AutoMapper // Map Codecrete Bill to DTO using AutoMapper
var qrCodeDto = mapper.Map<SwissQrCodeDataDto>(qrCodeData); var billDto = mapper.Map<SwissQrBillDto>(bill);
// Return references (passed through) + QR code data // Return references (passed through) + Bill DTO + raw lines
return new SwissQrCodeExtractionResult( return new SwissQrCodeExtractionResult(
References: request.References ?? [], Bill: billDto,
QrCodeData: qrCodeDto RawLines: rawLines
); );
} }
} }