Add Swiss QR Code extraction feature
Introduced DTOs for request and response to handle Swiss QR Code extraction from PDF documents. Implemented `ExtractSwissQrCodeHandler` to process the extraction using `ISwissQrCodeProcessor`. Added validation for the query with `ExtractSwissQrCodeValidator`. Developed unit tests to ensure correct behavior for successful and failure scenarios.
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
namespace DocumentOperator.Application.Common.DTOs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request to extract Swiss QR Code from a PDF document.
|
||||||
|
/// The QR code must be located on the last page of the document.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="References">Array of reference strings to pass through in the response</param>
|
||||||
|
/// <param name="Base64Pdf">PDF document encoded as Base64 string</param>
|
||||||
|
public record ExtractSwissQrCodeRequest(
|
||||||
|
IReadOnlyList<string> References,
|
||||||
|
string Base64Pdf
|
||||||
|
);
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
namespace DocumentOperator.Application.Common.DTOs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response containing extracted Swiss QR Code data and passed-through references.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="References">Reference strings passed through from the request</param>
|
||||||
|
/// <param name="QrCodeData">Parsed Swiss QR Code data from the last page of the PDF</param>
|
||||||
|
public record ExtractSwissQrCodeResponse(
|
||||||
|
IReadOnlyList<string> References,
|
||||||
|
SwissQrCodeDataDto QrCodeData
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Swiss QR Code data according to Swiss QR Bill Standard 2.0
|
||||||
|
/// </summary>
|
||||||
|
public record SwissQrCodeDataDto(
|
||||||
|
string QrType,
|
||||||
|
string Version,
|
||||||
|
string CodingType,
|
||||||
|
string Iban,
|
||||||
|
AddressDataDto Creditor,
|
||||||
|
AddressDataDto? UltimateCreditor,
|
||||||
|
decimal? Amount,
|
||||||
|
string Currency,
|
||||||
|
AddressDataDto? UltimateDebtor,
|
||||||
|
string ReferenceType,
|
||||||
|
string? Reference,
|
||||||
|
string? UnstructuredMessage,
|
||||||
|
string? BillInformation,
|
||||||
|
IReadOnlyList<string>? AlternativeProcedureParameters
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Address data (creditor or debtor) in Swiss QR Code
|
||||||
|
/// </summary>
|
||||||
|
public record AddressDataDto(
|
||||||
|
string AddressType,
|
||||||
|
string Name,
|
||||||
|
string? Street,
|
||||||
|
string? BuildingNumber,
|
||||||
|
string? AddressLine1,
|
||||||
|
string? AddressLine2,
|
||||||
|
string PostalCode,
|
||||||
|
string City,
|
||||||
|
string Country
|
||||||
|
);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using DocumentOperator.Application.Common.Interfaces;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DocumentOperator.Application.Features.Documents.ExtractSwissQrCode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles extraction of Swiss QR Code from PDF documents.
|
||||||
|
/// Uses ISwissQrCodeProcessor to extract QR code from the last page and parse it.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExtractSwissQrCodeHandler : IRequestHandler<ExtractSwissQrCodeQuery, ExtractSwissQrCodeResult>
|
||||||
|
{
|
||||||
|
private readonly ISwissQrCodeProcessor _qrCodeProcessor;
|
||||||
|
|
||||||
|
public ExtractSwissQrCodeHandler(ISwissQrCodeProcessor qrCodeProcessor)
|
||||||
|
{
|
||||||
|
_qrCodeProcessor = qrCodeProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ExtractSwissQrCodeResult> Handle(ExtractSwissQrCodeQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Convert Base64 string to byte array
|
||||||
|
byte[] pdfBytes = request.PdfContent.ToByteArray();
|
||||||
|
|
||||||
|
// Extract and parse Swiss QR Code from last page
|
||||||
|
var qrCodeData = await _qrCodeProcessor.ExtractSwissQrCodeAsync(pdfBytes, cancellationToken);
|
||||||
|
|
||||||
|
// Return references (passed through) + QR code data
|
||||||
|
return new ExtractSwissQrCodeResult(
|
||||||
|
References: request.References,
|
||||||
|
QrCodeData: qrCodeData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using DocumentOperator.Domain.Models.ValueObjects;
|
||||||
|
using DocumentOperator.Domain.ValueObjects;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DocumentOperator.Application.Features.Documents.ExtractSwissQrCode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query to extract Swiss QR Code data from a PDF document.
|
||||||
|
/// Returns references (passed through) and parsed QR code data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="References">Array of reference strings to pass through in the response</param>
|
||||||
|
/// <param name="PdfContent">PDF document content as Base64 string</param>
|
||||||
|
public record ExtractSwissQrCodeQuery(
|
||||||
|
IReadOnlyList<string> References,
|
||||||
|
Base64String PdfContent
|
||||||
|
) : IRequest<ExtractSwissQrCodeResult>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result containing passed-through references and extracted Swiss QR Code data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="References">Reference strings passed through from request</param>
|
||||||
|
/// <param name="QrCodeData">Parsed Swiss QR Code data from the last page of the PDF</param>
|
||||||
|
public record ExtractSwissQrCodeResult(
|
||||||
|
IReadOnlyList<string> References,
|
||||||
|
SwissQrCodeData QrCodeData
|
||||||
|
);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
|
||||||
|
namespace DocumentOperator.Application.Features.Documents.ExtractSwissQrCode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates ExtractSwissQrCodeQuery before handler execution.
|
||||||
|
/// Ensures references array and PDF content are provided.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExtractSwissQrCodeValidator : AbstractValidator<ExtractSwissQrCodeQuery>
|
||||||
|
{
|
||||||
|
public ExtractSwissQrCodeValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.References)
|
||||||
|
.NotNull()
|
||||||
|
.WithMessage("References array is required.");
|
||||||
|
|
||||||
|
RuleFor(x => x.PdfContent)
|
||||||
|
.NotNull()
|
||||||
|
.WithMessage("PDF content is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using DocumentOperator.Application.Common.Interfaces;
|
||||||
|
using DocumentOperator.Application.Features.Documents.ExtractSwissQrCode;
|
||||||
|
using DocumentOperator.Domain.Models.ValueObjects;
|
||||||
|
using DocumentOperator.Domain.ValueObjects;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace DocumentOperator.Tests.Unit.Application.Features.ExtractSwissQrCode;
|
||||||
|
|
||||||
|
public sealed class ExtractSwissQrCodeHandlerTests
|
||||||
|
{
|
||||||
|
private readonly Mock<ISwissQrCodeProcessor> _mockQrCodeProcessor;
|
||||||
|
private readonly ExtractSwissQrCodeHandler _handler;
|
||||||
|
|
||||||
|
public ExtractSwissQrCodeHandlerTests()
|
||||||
|
{
|
||||||
|
_mockQrCodeProcessor = new Mock<ISwissQrCodeProcessor>();
|
||||||
|
_handler = new ExtractSwissQrCodeHandler(_mockQrCodeProcessor.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_ValidRequest_ReturnsQrCodeDataAndReferences()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var references = new List<string> { "REF-001", "REF-002" };
|
||||||
|
var pdfBase64 = "JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovUGFnZXMgMiAwIFIKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL1R5cGUgL1BhZ2VzCi9LaWRzIFszIDAgUl0KL0NvdW50IDEKPj4KZW5kb2JqCjMgMCBvYmoKPDwKL1R5cGUgL1BhZ2UKL1BhcmVudCAyIDAgUgovTWVkaWFCb3ggWzAgMCA2MTIgNzkyXQovQ29udGVudHMgNCAwIFIKPj4KZW5kb2JqCjQgMCBvYmoKPDwKL0xlbmd0aCAzMgo+PgpzdHJlYW0KQlQKL0YxIDEyIFRmCjEwMCA3MDAgVGQKKEhlbGxvKSBUagpFVAplbmRzdHJlYW0KZW5kb2JqCnhyZWYKMCA1CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDAxNSAwMDAwMCBuIAowMDAwMDAwMDY0IDAwMDAwIG4gCjAwMDAwMDAxMjEgMDAwMDAgbiAKMDAwMDAwMDIxMyAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDUKL1Jvb3QgMSAwIFIKPj4Kc3RhcnR4cmVmCjI5NAolJUVPRgo=";
|
||||||
|
var base64String = Base64String.Create(pdfBase64);
|
||||||
|
var query = new ExtractSwissQrCodeQuery(references, base64String);
|
||||||
|
|
||||||
|
var expectedQrCodeData = new SwissQrCodeData
|
||||||
|
{
|
||||||
|
QrType = "SPC",
|
||||||
|
Version = "0200",
|
||||||
|
CodingType = "1",
|
||||||
|
Iban = "CH4431999123000889012",
|
||||||
|
Creditor = new AddressData
|
||||||
|
{
|
||||||
|
AddressType = "S",
|
||||||
|
Name = "Robert Schneider AG",
|
||||||
|
Street = "Rue du Lac",
|
||||||
|
BuildingNumber = "1268",
|
||||||
|
PostalCode = "2501",
|
||||||
|
City = "Biel",
|
||||||
|
Country = "CH"
|
||||||
|
},
|
||||||
|
UltimateCreditor = null,
|
||||||
|
Amount = 1949.75m,
|
||||||
|
Currency = "CHF",
|
||||||
|
UltimateDebtor = null,
|
||||||
|
ReferenceType = "QRR",
|
||||||
|
Reference = "210000000003139471430009017",
|
||||||
|
UnstructuredMessage = "Order from 15.01.2025",
|
||||||
|
BillInformation = null,
|
||||||
|
AlternativeProcedureParameters = null
|
||||||
|
};
|
||||||
|
|
||||||
|
_mockQrCodeProcessor
|
||||||
|
.Setup(x => x.ExtractSwissQrCodeAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(expectedQrCodeData);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _handler.Handle(query, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
result.Should().NotBeNull();
|
||||||
|
result.References.Should().BeEquivalentTo(references);
|
||||||
|
result.QrCodeData.Should().BeEquivalentTo(expectedQrCodeData);
|
||||||
|
|
||||||
|
_mockQrCodeProcessor.Verify(
|
||||||
|
x => x.ExtractSwissQrCodeAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()),
|
||||||
|
Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Handle_QrCodeProcessorThrowsException_PropagatesException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var references = new List<string> { "REF-001" };
|
||||||
|
var pdfBase64 = "JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovUGFnZXMgMiAwIFIKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL1R5cGUgL1BhZ2VzCi9LaWRzIFszIDAgUl0KL0NvdW50IDEKPj4KZW5kb2JqCjMgMCBvYmoKPDwKL1R5cGUgL1BhZ2UKL1BhcmVudCAyIDAgUgovTWVkaWFCb3ggWzAgMCA2MTIgNzkyXQovQ29udGVudHMgNCAwIFIKPj4KZW5kb2JqCjQgMCBvYmoKPDwKL0xlbmd0aCAzMgo+PgpzdHJlYW0KQlQKL0YxIDEyIFRmCjEwMCA3MDAgVGQKKEhlbGxvKSBUagpFVAplbmRzdHJlYW0KZW5kb2JqCnhyZWYKMCA1CjAwMDAwMDAwMDAgNjU1MzUgZiAKMDAwMDAwMDAxNSAwMDAwMCBuIAowMDAwMDAwMDY0IDAwMDAwIG4gCjAwMDAwMDAxMjEgMDAwMDAgbiAKMDAwMDAwMDIxMyAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDUKL1Jvb3QgMSAwIFIKPj4Kc3RhcnR4cmVmCjI5NAolJUVPRgo=";
|
||||||
|
var base64String = Base64String.Create(pdfBase64);
|
||||||
|
var query = new ExtractSwissQrCodeQuery(references, base64String);
|
||||||
|
|
||||||
|
_mockQrCodeProcessor
|
||||||
|
.Setup(x => x.ExtractSwissQrCodeAsync(It.IsAny<byte[]>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ThrowsAsync(new InvalidOperationException("QR Code processing failed"));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
Func<Task> act = async () => await _handler.Handle(query, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||||
|
.WithMessage("QR Code processing failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user