Refactor: Rename DocumentOperator to DocumentService

Updated project and solution files to rename and restructure
`DocumentOperator` to `DocumentService`. Modified project
references in `DocumentService.API.csproj`, `DocumentService.Application.csproj`,
and `DocumentService.Infrastructure.csproj` to reflect the new
naming convention. Updated `DocumentService.sln` to remove
old project references, add new ones, and adjust configuration
and nested project mappings accordingly. This change aligns
the project structure with the new naming convention.
This commit is contained in:
2026-08-30 21:33:25 +02:00
parent 050cb55bf9
commit 207c778f3a
108 changed files with 37 additions and 37 deletions

View File

@@ -0,0 +1,56 @@
using MediatR;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
namespace DocumentService.Application.Common.Behaviors;
/// <summary>
/// MediatR Pipeline Behavior that logs requests and tracks performance
/// Executes AFTER ValidationBehavior, BEFORE Handler
/// </summary>
public class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> Logger) : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
var requestName = typeof(TRequest).Name;
// Performance Tracking
var stopwatch = Stopwatch.StartNew();
try
{
// Handler ausführen
var response = await next(cancellationToken);
stopwatch.Stop();
// Request Success
Logger.LogInformation(
"Handled {RequestName} in {ElapsedMs}ms",
requestName,
stopwatch.ElapsedMilliseconds
);
return response;
}
catch (Exception ex)
{
stopwatch.Stop();
// Request Failed
Logger.LogError(
ex,
"Error handling {RequestName} after {ElapsedMs}ms: {ErrorMessage}",
requestName,
stopwatch.ElapsedMilliseconds,
ex.Message
);
throw; // Exception weiterwerfen (wird von Exception Middleware gefangen)
}
}
}

View File

@@ -0,0 +1,54 @@
using FluentValidation;
using MediatR;
namespace DocumentService.Application.Common.Behaviors;
/// <summary>
/// MediatR Pipeline Behavior that validates requests using FluentValidation
/// Executes BEFORE the Handler
/// </summary>
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
// Wenn keine Validators registriert sind, direkt weiter zum Handler
if (!_validators.Any())
{
return await next();
}
// Validation Context erstellen
var context = new ValidationContext<TRequest>(request);
// Alle Validators parallel ausführen
var validationResults = await Task.WhenAll(
_validators.Select(v => v.ValidateAsync(context, cancellationToken))
);
// Fehler sammeln
var failures = validationResults
.Where(r => !r.IsValid)
.SelectMany(r => r.Errors)
.ToList();
// Bei Fehlern: ValidationException werfen (wird von Exception Middleware gefangen)
if (failures.Any())
{
throw new ValidationException(failures);
}
// Validation erfolgreich ? weiter zum Handler
return await next();
}
}

View File

@@ -0,0 +1,29 @@
namespace DocumentService.Application.Common.Configuration;
/// <summary>
/// Configuration settings for ZUGFeRD/Factur-X/XRechnung detection
/// </summary>
public class ZugferdSettings
{
/// <summary>
/// Exact ZUGFeRD/Factur-X/XRechnung file names to check (case-insensitive)
/// </summary>
public List<string> ZugferdFileNames { get; set; } = new()
{
"factur-x.xml",
"zugferd-invoice.xml",
"ZUGFeRD-invoice.xml",
"xrechnung.xml"
};
/// <summary>
/// Partial filename patterns for ZUGFeRD detection (case-insensitive)
/// </summary>
public List<string> ZugferdFileNamePatterns { get; set; } = new()
{
"factur",
"zugferd",
"xrechnung",
"peppol"
};
}

View File

@@ -0,0 +1,43 @@
namespace DocumentService.Application.Common.DTOs;
/// <summary>
/// DTO for attachment check result returned to API layer
/// </summary>
public record AttachmentCheckResult
{
/// <summary>
/// Indicates whether the PDF contains any attachments
/// </summary>
public bool HasAttachments { get; init; }
/// <summary>
/// Total number of attachments in the PDF
/// </summary>
public int AttachmentCount { get; init; }
/// <summary>
/// List of attachment metadata (file details)
/// </summary>
public List<AttachmentDto> Attachments { get; init; } = new();
}
/// <summary>
/// DTO for individual attachment metadata
/// </summary>
public record AttachmentDto
{
/// <summary>
/// Attachment file name (e.g., "invoice.xml", "document.pdf")
/// </summary>
public string FileName { get; init; } = string.Empty;
/// <summary>
/// MIME type of the attachment (e.g., "text/xml", "application/pdf")
/// </summary>
public string MimeType { get; init; } = string.Empty;
/// <summary>
/// Attachment file size in bytes
/// </summary>
public long Size { get; init; }
}

View File

@@ -0,0 +1,60 @@
namespace DocumentService.Application.Common.DTOs;
/// <summary>
/// Represents complete attachment information for a PDF document.
/// Immutable value object containing attachment presence flag, count, and detailed metadata.
/// </summary>
public sealed class AttachmentInfo
{
/// <summary>
/// Gets a value indicating whether the PDF contains any attachments
/// </summary>
public bool HasAttachments { get; }
/// <summary>
/// Gets the total number of attachments in the PDF
/// </summary>
public int AttachmentCount { get; }
/// <summary>
/// Gets the collection of attachment metadata (file details)
/// </summary>
public IReadOnlyList<AttachmentMetadata> Attachments { get; }
/// <summary>
/// Initializes a new instance of the AttachmentInfo class.
/// </summary>
/// <param name="hasAttachments">Whether PDF has attachments</param>
/// <param name="attachmentCount">Total number of attachments</param>
/// <param name="attachments">List of attachment metadata (can be empty)</param>
public AttachmentInfo(bool hasAttachments, int attachmentCount, IReadOnlyList<AttachmentMetadata> attachments)
{
HasAttachments = hasAttachments;
AttachmentCount = attachmentCount;
Attachments = attachments ?? [];
// Defensive Programming: Ensure count matches list length
if (Attachments.Count != attachmentCount)
{
throw new ArgumentException(
$"Attachment count mismatch: expected {attachmentCount}, got {Attachments.Count}",
nameof(attachments));
}
}
/// <summary>
/// Creates an AttachmentInfo instance for a PDF with no attachments.
/// </summary>
public static AttachmentInfo Empty =>
new(
hasAttachments: false,
attachmentCount: 0,
attachments: []);
public override string ToString()
{
return HasAttachments
? $"PDF has {AttachmentCount} attachment(s): {string.Join(", ", Attachments.Select(a => a.FileName))}"
: "PDF has no attachments";
}
}

View File

@@ -0,0 +1,44 @@
namespace DocumentService.Application.Common.DTOs;
/// <summary>
/// Represents metadata of a single PDF attachment (embedded file).
/// Immutable value object containing file information without the actual binary data.
/// </summary>
/// <remarks>
/// Initializes a new instance of the AttachmentMetadata class.
/// </remarks>
/// <param name="fileName">Attachment file name</param>
/// <param name="mimeType">MIME type (e.g., "text/xml")</param>
/// <param name="sizeBytes">File size in bytes</param>
public sealed class AttachmentMetadata(string fileName, string mimeType, long sizeBytes)
{
/// <summary>
/// Gets the attachment file name (e.g., "invoice.xml", "document.pdf")
/// </summary>
public string FileName { get; } = fileName ?? string.Empty;
/// <summary>
/// Gets the MIME type of the attachment (e.g., "text/xml", "application/pdf")
/// </summary>
public string MimeType { get; } = mimeType ?? "application/octet-stream"; // Default MIME type if unknown
/// <summary>
/// Gets the attachment file size in bytes
/// </summary>
public long SizeBytes { get; } = sizeBytes;
/// <summary>
/// Gets the attachment file size in kilobytes (computed property)
/// </summary>
public double SizeKB => SizeBytes / 1024.0;
/// <summary>
/// Gets the attachment file size in megabytes (computed property)
/// </summary>
public double SizeMB => SizeBytes / 1024.0 / 1024.0;
public override string ToString()
{
return $"{FileName} ({MimeType}, {SizeKB:F2} KB)";
}
}

View File

@@ -0,0 +1,52 @@
namespace DocumentService.Application.Common.DTOs;
/// <summary>
/// PDF/A validation result including conformance level and validation errors/warnings
/// </summary>
public record PdfAValidationResult
{
/// <summary>
/// Whether the PDF is valid (no errors)
/// </summary>
public bool IsValid { get; init; }
/// <summary>
/// PDF version (e.g., "1.4", "1.7")
/// </summary>
public string PdfVersion { get; init; } = string.Empty;
/// <summary>
/// Number of pages in the PDF
/// </summary>
public int PageCount { get; init; }
/// <summary>
/// File size in bytes
/// </summary>
public long FileSize { get; init; }
/// <summary>
/// Whether the PDF is encrypted
/// </summary>
public bool Encrypted { get; init; }
/// <summary>
/// PDF/A version (e.g., "PDF/A-1b", "PDF/A-2a", "PDF/A-3u") or null if not PDF/A compliant
/// </summary>
public string? PdfAVersion { get; init; }
/// <summary>
/// Whether the PDF conforms to PDF/A standard
/// </summary>
public bool PdfACompliant { get; init; }
/// <summary>
/// Validation errors (e.g., "PDF/A documents cannot be encrypted")
/// </summary>
public IReadOnlyList<string> Errors { get; init; } = [];
/// <summary>
/// Validation warnings (e.g., "Manual verification recommended: All fonts must be embedded")
/// </summary>
public IReadOnlyList<string> Warnings { get; init; } = [];
}

View File

@@ -0,0 +1,20 @@
namespace DocumentService.Application.Common.DTOs;
/// <summary>
/// Response mit PDF-Metadaten
/// </summary>
/// <param name="PageCount">Anzahl der Seiten</param>
/// <param name="FileSizeBytes">Dateigröße in Bytes</param>
/// <param name="FileSizeMB">Dateigröße in MB (gerundet auf 2 Dezimalstellen)</param>
/// <param name="PdfVersion">PDF-Version (z.B. "1.4")</param>
/// <param name="HasAttachments">Hat das PDF Anhänge?</param>
/// <param name="AttachmentCount">Anzahl der Anhänge</param>
/// <param name="IsEncrypted">Ist das PDF passwortgeschützt/verschlüsselt?</param>
public record PdfValidationResult(
int PageCount,
long FileSizeBytes,
double FileSizeMB,
string PdfVersion,
bool HasAttachments,
int AttachmentCount,
bool IsEncrypted);

View File

@@ -0,0 +1,93 @@
namespace DocumentService.Application.Common.DTOs;
/// <summary>
/// Swiss QR Bill data transfer object (mapped from Codecrete Bill)
/// </summary>
public record SwissQrBillDto
{
/// <summary>QR Bill standard version (e.g., "V2_0")</summary>
public required string Version { get; init; }
/// <summary>Payment amount (null if not specified)</summary>
public decimal? Amount { get; init; }
/// <summary>Payment currency (CHF or EUR)</summary>
public required string Currency { get; init; }
/// <summary>Creditor's IBAN account number</summary>
public required string Account { get; init; }
/// <summary>Creditor address</summary>
public required AddressDto Creditor { get; init; }
/// <summary>Payment 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>Debtor address (optional)</summary>
public AddressDto? Debtor { get; init; }
/// <summary>Additional unstructured message (max 140 chars)</summary>
public string? UnstructuredMessage { get; init; }
/// <summary>Additional structured bill information</summary>
public string? BillInformation { get; init; }
/// <summary>Alternative payment schemes (max 2)</summary>
public IReadOnlyList<AlternativeSchemeDto>? AlternativeSchemes { get; init; }
}
/// <summary>
/// Address data transfer object (mapped from Codecrete Address)
/// </summary>
public record AddressDto
{
/// <summary>Address type: "Structured" or "CombinedElements"</summary>
public required string Type { 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>House number (structured address only)</summary>
public string? HouseNo { get; init; }
/// <summary>
/// Address line 1 (combined address only).
/// OBSOLETE: Use structured address instead. Will be removed when Codecrete v4 is adopted.
/// </summary>
[Obsolete("Use structured address (Street + HouseNo) instead. This field will be removed when upgrading to Codecrete.SwissQRBill.Generator v4.x")]
public string? AddressLine1 { get; init; }
/// <summary>
/// Address line 2 (combined address only).
/// OBSOLETE: Use structured address instead. Will be removed when Codecrete v4 is adopted.
/// </summary>
[Obsolete("Use structured address (Street + HouseNo) instead. This field will be removed when upgrading to Codecrete.SwissQRBill.Generator v4.x")]
public string? AddressLine2 { get; init; }
/// <summary>Postal code</summary>
public required string PostalCode { get; init; }
/// <summary>Town/city name</summary>
public required string Town { get; init; }
/// <summary>Two-letter country code (ISO 3166-1 alpha-2)</summary>
public required string CountryCode { get; init; }
}
/// <summary>
/// Alternative payment scheme (mapped from Codecrete AlternativeScheme)
/// </summary>
public record AlternativeSchemeDto
{
/// <summary>Scheme name (e.g., "AV1", "AV2")</summary>
public required string Name { get; init; }
/// <summary>Scheme instruction/parameter</summary>
public required string Instruction { get; init; }
}

View File

@@ -0,0 +1,37 @@
namespace DocumentService.Application.Common.DTOs;
/// <summary>
/// Response containing extracted Swiss QR Code in dual format.
/// </summary>
/// <param name="Bill">Parsed Swiss QR Bill (structured format)</param>
/// <param name="RawLines">Raw Swiss QR Code lines (original newline-separated format)</param>
/// <example>
/// {
/// "bill": {
/// "version": "V2_0",
/// "amount": 630.20,
/// "currency": "CHF",
/// "account": "CH953000520280564701R",
/// "creditor": {
/// "type": "Structured",
/// "name": "ALMAT AG",
/// "town": "Tagelswangen"
/// },
/// "referenceType": "QRR",
/// "reference": "000000000000000000252080824"
/// },
/// "rawLines": [
/// "SPC",
/// "0200",
/// "1",
/// "CH953000520280564701R",
/// "S",
/// "ALMAT AG",
/// "..."
/// ]
/// }
/// </example>
public record SwissQrCodeExtractionResult(
SwissQrBillDto Bill,
IReadOnlyList<string> RawLines
);

View File

@@ -0,0 +1,27 @@
namespace DocumentService.Application.Common.DTOs;
/// <summary>
/// DTO for ZUGFeRD check result
/// </summary>
public record ZugferdCheckResult
{
/// <summary>
/// Indicates whether the PDF contains ZUGFeRD XML attachment
/// </summary>
public bool HasZugferd { get; init; }
/// <summary>
/// ZUGFeRD XML file name (e.g., "factur-x.xml")
/// </summary>
public string? ZugferdFileName { get; init; }
/// <summary>
/// ZUGFeRD XML file size in bytes
/// </summary>
public long? ZugferdFileSize { get; init; }
/// <summary>
/// MIME type of the ZUGFeRD XML file
/// </summary>
public string? ZugferdMimeType { get; init; }
}

View File

@@ -0,0 +1,191 @@
using DocumentService.Application.Common.DTOs;
namespace DocumentService.Application.Common.Interfaces;
public interface IPdfProcessor
{
/// <summary>
/// Validates a PDF and extracts metadata.
/// </summary>
/// <param name="pdfStream">
/// PDF document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </param>
/// <returns>PDF metadata (page count, size, version, attachments)</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty, invalid, or not positioned at the beginning
/// </exception>
Task<PdfValidationResult> ValidateAsync(Stream pdfStream);
/// <summary>
/// Validates a PDF/A document and checks conformance level.
/// </summary>
/// <param name="pdfStream">
/// PDF document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </param>
/// <returns>PDF/A metadata including conformance level and validation errors/warnings</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty, invalid, or not positioned at the beginning
/// </exception>
Task<PdfAValidationResult> ValidatePdfAAsync(Stream pdfStream);
/// <summary>
/// Checks for embedded files (attachments) in a PDF document and returns detailed metadata.
/// </summary>
/// <param name="pdfStream">
/// PDF document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </param>
/// <returns>Attachment information (count, file names, MIME types, sizes)</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty, invalid, or not positioned at the beginning
/// </exception>
Task<AttachmentInfo> CheckAttachmentsAsync(Stream pdfStream);
/// <summary>
/// Extracts all embedded files from a PDF document and returns them as a ZIP archive.
/// </summary>
/// <param name="pdfStream">
/// PDF document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </param>
/// <returns>ZIP archive containing all extracted attachments as byte array</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty, invalid, or not positioned at the beginning
/// </exception>
/// <exception cref="Domain.Common.Exceptions.NotFoundException">
/// Thrown when PDF contains no attachments
/// </exception>
Task<byte[]> ExtractAttachmentsAsync(Stream pdfStream);
/// <summary>
/// Merges multiple PDF documents into a single PDF.
/// </summary>
/// <param name="pdfStreams">
/// PDF streams to merge (minimum 2 required). Each stream must be readable and positioned
/// at the beginning (Position = 0). Caller is responsible for disposal.
/// </param>
/// <param name="pageRanges">
/// Optional page ranges per PDF (null = all pages). Format: "1-3,5" means pages 1, 2, 3, and 5.
/// If null or empty for a PDF, all pages are included. Array length must match pdfStreams length if provided.
/// </param>
/// <returns>Merged PDF as byte array</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when fewer than 2 PDFs provided, any stream is empty/invalid/not at Position=0,
/// or page range format is invalid
/// </exception>
Task<byte[]> MergePdfsAsync(IReadOnlyList<Stream> pdfStreams, IReadOnlyList<string?>? pageRanges = null);
/// <summary>
/// Adds an annotation to a PDF document at the specified page and rectangle.
/// </summary>
/// <param name="pdfStream">
/// PDF document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </param>
/// <param name="annotationType">Type of annotation to add (TextMarkup, FreeText, StickyNote, Circle, Square)</param>
/// <param name="pageNumber">Page number (1-based) where annotation should be added</param>
/// <param name="rectangle">Annotation bounding rectangle (X1, Y1, X2, Y2)</param>
/// <param name="content">Annotation content/comment text (required for FreeText and StickyNote)</param>
/// <param name="author">Optional author name</param>
/// <param name="color">Optional annotation color in RGB format (hex string like "FF0000" for red)</param>
/// <param name="textMarkupStyle">Text markup style (Highlight, Underline, Strikeout) - only for TextMarkup type</param>
/// <param name="origin">Coordinate origin (BottomLeft = PDF native, TopLeft = UI-friendly). Default: BottomLeft</param>
/// <returns>Annotated PDF as byte array</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty/invalid/not at Position=0, page number out of range,
/// rectangle invalid, or content missing for types that require it
/// </exception>
Task<byte[]> AddAnnotationAsync(
Stream pdfStream,
Domain.Models.ValueObjects.AnnotationType annotationType,
int pageNumber,
(double X1, double Y1, double X2, double Y2) rectangle,
string? content = null,
string? author = null,
string? color = null,
Domain.Models.ValueObjects.TextMarkupStyle? textMarkupStyle = null,
Domain.Models.ValueObjects.AnnotationOrigin origin = Domain.Models.ValueObjects.AnnotationOrigin.BottomLeft);
/// <summary>
/// Adds a stamp (text, image, or predefined) to specified pages of a PDF document.
/// </summary>
/// <param name="pdfStream">Input PDF stream (must support reading and seeking)</param>
/// <param name="stampType">Type of stamp (Text, Image, or Predefined)</param>
/// <param name="pageNumbers">Target page numbers (1-based). Null = all pages.</param>
/// <param name="position">Stamp position (X, Y coordinates)</param>
/// <param name="size">Stamp size (Width, Height). Null = auto-size for images.</param>
/// <param name="origin">Coordinate origin (BottomLeft or TopLeft)</param>
/// <param name="text">Text content (required for Text stamps)</param>
/// <param name="fontName">Font name (default: Arial)</param>
/// <param name="fontSize">Font size in points (default: 12)</param>
/// <param name="color">Hex color without # (e.g., "FF0000" for red, default: "000000")</param>
/// <param name="opacity">Opacity 0.0 (transparent) to 1.0 (opaque, default: 0.5)</param>
/// <param name="rotation">Rotation angle in degrees 0-360 (default: 0)</param>
/// <param name="placement">Foreground (on top) or Background (behind content)</param>
/// <param name="imageBytes">Image data (required for Image stamps, PNG/JPEG)</param>
/// <param name="predefinedType">Predefined stamp type (required for Predefined stamps)</param>
/// <returns>Stamped PDF as byte array</returns>
/// <exception cref="BadRequestException">Invalid parameters (missing text/image, invalid page numbers, invalid opacity/rotation)</exception>
/// <exception cref="PdfProcessingException">DevExpress processing error</exception>
Task<byte[]> AddStampAsync(
Stream pdfStream,
Domain.Models.ValueObjects.StampType stampType,
int[]? pageNumbers,
(double X, double Y) position,
(double Width, double Height)? size = null,
Domain.Models.ValueObjects.AnnotationOrigin origin = Domain.Models.ValueObjects.AnnotationOrigin.BottomLeft,
string? text = null,
string? fontName = null,
double? fontSize = null,
string? color = null,
double? opacity = null,
double? rotation = null,
Domain.Models.ValueObjects.StampPlacement placement = Domain.Models.ValueObjects.StampPlacement.Foreground,
byte[]? imageBytes = null,
Domain.Models.ValueObjects.PredefinedStampType? predefinedType = null);
/// <summary>
/// Embeds one or more files as attachments in a PDF document (supports PDF/A-3).
/// </summary>
/// <param name="pdfStream">
/// PDF document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </param>
/// <param name="attachments">List of files to embed (filename, content, optional MIME type)</param>
/// <returns>PDF with embedded attachments as byte array</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty/invalid, or attachments list is empty
/// </exception>
Task<byte[]> AddAttachmentsAsync(
Stream pdfStream,
IReadOnlyList<(string FileName, byte[] Content, string? MimeType)> attachments);
/// <summary>
/// Converts a standard PDF to PDF/A format.
/// </summary>
/// <param name="pdfStream">
/// PDF document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </param>
/// <param name="pdfALevel">Target PDF/A level (e.g., "PDF/A-1b", "PDF/A-2b", "PDF/A-3b")</param>
/// <returns>PDF/A compliant document as byte array</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty/invalid or PDF/A level is unsupported
/// </exception>
Task<byte[]> ConvertToPdfAAsync(Stream pdfStream, string pdfALevel);
/// <summary>
/// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions).
/// </summary>
/// <param name="pdfStream">
/// PDF/A document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </param>
/// <returns>Standard PDF document as byte array</returns>
/// <exception cref="Domain.Common.Exceptions.BadRequestException">
/// Thrown when stream is empty/invalid
/// </exception>
Task<byte[]> ConvertFromPdfAAsync(Stream pdfStream);
}

View File

@@ -0,0 +1,32 @@
using Codecrete.SwissQRBill.Generator;
namespace DocumentService.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 a PDF document.
/// Returns both parsed Bill object and raw QR text lines.
/// </summary>
/// <param name="pdfStream">
/// PDF document stream. Must be readable and positioned at the beginning (Position = 0).
/// Non-seekable streams are supported. Caller is responsible for disposal.
/// </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>
/// <returns>Tuple: (Parsed Codecrete Bill, Raw QR lines as string array)</returns>
/// <exception cref="Domain.Common.Exceptions.NotFoundException">
/// Thrown when no Swiss QR Code is found in the specified pages
/// </exception>
/// <exception cref="ArgumentException">
/// Thrown when stream is empty or (for seekable streams) not positioned at the beginning
/// </exception>
Task<(Bill Bill, string[] RawLines)> ExtractSwissQrCodeAsync(
Stream pdfStream,
int[]? pageNumbers = null,
CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,48 @@
using AutoMapper;
using Codecrete.SwissQRBill.Generator;
using DocumentService.Application.Common.DTOs;
using DocumentService.Domain.Models.ValueObjects;
namespace DocumentService.Application.Common.Mapping;
/// <summary>
/// AutoMapper profile for mapping domain entities and external models to DTOs.
/// NOTE: Always use AutoMapper for all mappings in this project.
/// </summary>
public class MappingProfile : Profile
{
public MappingProfile()
{
// Codecrete Bill -> SwissQrBillDto
CreateMap<Bill, SwissQrBillDto>()
.ForMember(dest => dest.Version, opt => opt.MapFrom(src => src.Version.ToString()))
.ForMember(dest => dest.Currency, opt => opt.MapFrom(src => src.Currency ?? "CHF"))
.ForMember(dest => dest.Account, opt => opt.MapFrom(src => src.Account ?? string.Empty))
.ForMember(dest => dest.ReferenceType, opt => opt.MapFrom(src => src.ReferenceType ?? Bill.ReferenceTypeNoRef));
// Codecrete Address -> AddressDto
CreateMap<Address, AddressDto>()
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type.ToString()))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name ?? string.Empty))
.ForMember(dest => dest.PostalCode, opt => opt.MapFrom(src => src.PostalCode ?? string.Empty))
.ForMember(dest => dest.Town, opt => opt.MapFrom(src => src.Town ?? string.Empty))
.ForMember(dest => dest.CountryCode, opt => opt.MapFrom(src => src.CountryCode ?? string.Empty))
#pragma warning disable CS0618 // Suppress obsolete warning for AddressLine1/2 mapping
.ForMember(dest => dest.AddressLine1, opt => opt.MapFrom(src => src.AddressLine1))
.ForMember(dest => dest.AddressLine2, opt => opt.MapFrom(src => src.AddressLine2));
#pragma warning restore CS0618
// Codecrete AlternativeScheme -> AlternativeSchemeDto
CreateMap<AlternativeScheme, AlternativeSchemeDto>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name ?? string.Empty))
.ForMember(dest => dest.Instruction, opt => opt.MapFrom(src => src.Instruction ?? string.Empty));
// AttachmentInfo -> AttachmentCheckResult
CreateMap<AttachmentInfo, AttachmentCheckResult>();
// AttachmentMetadata -> AttachmentDto
CreateMap<AttachmentMetadata, AttachmentDto>()
.ForMember(dest => dest.Size, opt => opt.MapFrom(src => src.SizeBytes));
}
}