Compare commits
10 Commits
2804993ea6
...
3da6ca8323
| Author | SHA1 | Date | |
|---|---|---|---|
| 3da6ca8323 | |||
| 0e88b349d7 | |||
| b6bb894257 | |||
| 9cff7ee459 | |||
| a6694bfce7 | |||
| a242458d2f | |||
| 94123cd1be | |||
| 080c2ac2a0 | |||
| 97122d0bbd | |||
| e9d1586266 |
12
AGENTS.md
12
AGENTS.md
@@ -1,6 +1,6 @@
|
||||
# AGENTS.md
|
||||
|
||||
Agent guidance for DocumentOperator service. Read this before working on the codebase.
|
||||
Agent guidance for DocumentService service. Read this before working on the codebase.
|
||||
|
||||
---
|
||||
|
||||
@@ -23,7 +23,7 @@ Agent guidance for DocumentOperator service. Read this before working on the cod
|
||||
### Migration Required
|
||||
|
||||
**Existing code that needs replacement:**
|
||||
- `DocumentOperator.API/Endpoints/v1/DocumentEndpoints.cs` → Delete, replace with Controllers
|
||||
- `DocumentService.API/Endpoints/v1/DocumentEndpoints.cs` → Delete, replace with Controllers
|
||||
- `Program.cs` line 72: `app.MapDocumentEndpoints()` → Replace with `app.MapControllers()`
|
||||
- `Program.cs` line 44: Add `builder.Services.AddControllers()`
|
||||
- All DTOs → Support **BOTH** `IFormFile` (multipart) AND `Base64String` (JSON)
|
||||
@@ -195,7 +195,7 @@ dotnet test
|
||||
|
||||
**Run API (Development):**
|
||||
```powershell
|
||||
dotnet run --project DocumentOperator.API
|
||||
dotnet run --project DocumentService.API
|
||||
```
|
||||
Swagger UI: `https://localhost:7186/swagger`
|
||||
Serilog UI: `https://localhost:7186/serilog-ui` (Web-based log viewer)
|
||||
@@ -339,7 +339,7 @@ Do NOT skip steps. Each feature is done when it's **testable in Swagger UI with
|
||||
**All test PDFs are EmbeddedResource.** Access via:
|
||||
```csharp
|
||||
var stream = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("DocumentOperator.Tests.TestData.Pdfs.valid.pdf");
|
||||
.GetManifestResourceStream("DocumentService.Tests.TestData.Pdfs.valid.pdf");
|
||||
```
|
||||
|
||||
**Do NOT commit new binary files** without marking them as `<EmbeddedResource>`.
|
||||
@@ -351,7 +351,7 @@ var stream = Assembly.GetExecutingAssembly()
|
||||
**3-folder structure (CORRECT approach by previous developer):**
|
||||
|
||||
```
|
||||
DocumentOperator.Tests/
|
||||
DocumentService.Tests/
|
||||
├── Integration/
|
||||
│ └── API/
|
||||
│ ├── PdfValidationControllerTests.cs (13 tests)
|
||||
@@ -520,7 +520,7 @@ public async Task<IActionResult> Validate([FromForm] PdfInputModel input, Cancel
|
||||
## Configuration
|
||||
|
||||
**appsettings.json sections:**
|
||||
- `DocumentOperatorSettings` (future: file size limits, temp paths)
|
||||
- `DocumentServiceSettings` (future: file size limits, temp paths)
|
||||
- `RedisSettings` (future: multi-tenancy caching)
|
||||
- `ApiKeySettings` (future: authentication)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# DocumentOperator - Controller & Endpoint Specification
|
||||
# DocumentService - Controller & Endpoint Specification
|
||||
|
||||
**Project:** DocumentService (DOC)
|
||||
**Ticket:** DOC-1 - GDPicture and Nutrient Replacing
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This specification defines the controller structure and REST API endpoints for the DocumentOperator service.
|
||||
This specification defines the controller structure and REST API endpoints for the DocumentService service.
|
||||
|
||||
---
|
||||
|
||||
@@ -293,7 +293,7 @@ The service can be used on the client side **without manual HTTP response handli
|
||||
|
||||
**Example Client SDK:**
|
||||
```csharp
|
||||
var client = new DocumentOperatorClient("https://api.example.com");
|
||||
var client = new DocumentServiceClient("https://api.example.com");
|
||||
var result = await client.Pdf.Validation.ValidateAsync(pdfFile);
|
||||
if (result.IsValid) { ... }
|
||||
```
|
||||
|
||||
@@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace DocumentOperator.API.Configuration
|
||||
namespace DocumentService.API.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Swagger document filter that merges operations with same path but different [Consumes] attributes.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.API.Configuration
|
||||
namespace DocumentService.API.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Placeholder class for Serilog configuration extensions.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using System.Reflection;
|
||||
|
||||
namespace DocumentOperator.API.Configuration
|
||||
namespace DocumentService.API.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for configuring Swagger/OpenAPI documentation.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.API.Configuration;
|
||||
namespace DocumentService.API.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration settings for Swagger/OpenAPI documentation.
|
||||
@@ -19,7 +19,7 @@ public class SwaggerSettings
|
||||
/// <summary>
|
||||
/// API title displayed in Swagger UI.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = "DocumentOperator API";
|
||||
public string Title { get; set; } = "DocumentService API";
|
||||
|
||||
/// <summary>
|
||||
/// API version.
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using DocumentOperator.Application.CheckPdfAttachments.Queries;
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.ExtractPdfAttachments;
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using DocumentService.Application.AddAttachments;
|
||||
using DocumentService.Application.CheckPdfAttachments.Queries;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.ExtractPdfAttachments;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentOperator.API.Controllers;
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for PDF attachment operations (detection, extraction, embedding)
|
||||
@@ -157,6 +158,135 @@ public class PdfAttachmentController(IMediator mediator) : ControllerBase
|
||||
// Return ZIP file
|
||||
return File(zipBytes, "application/zip", "attachments.zip");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Embeds one or more files as attachments in a PDF document (supports PDF/A-3).
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="pdfFile">The PDF file to add attachments to</param>
|
||||
/// <param name="attachmentFiles">Files to embed as attachments (one or more)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF with embedded attachments</returns>
|
||||
/// <response code="200">Attachments added successfully - returns PDF</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF, or no attachments provided)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("add")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> AddAttachmentsFromFile(
|
||||
IFormFile pdfFile,
|
||||
List<IFormFile> attachmentFiles,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (pdfFile == null || pdfFile.Length == 0)
|
||||
{
|
||||
throw new BadRequestException("PDF file is required");
|
||||
}
|
||||
|
||||
if (attachmentFiles == null || attachmentFiles.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("At least one attachment file is required");
|
||||
}
|
||||
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = pdfFile.OpenReadStream();
|
||||
|
||||
// Convert attachment files to AttachmentFile records
|
||||
var attachments = new List<AttachmentFile>();
|
||||
foreach (var file in attachmentFiles)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.CopyToAsync(ms, cancellationToken);
|
||||
|
||||
attachments.Add(new AttachmentFile
|
||||
{
|
||||
FileName = file.FileName,
|
||||
Content = ms.ToArray(),
|
||||
MimeType = file.ContentType
|
||||
});
|
||||
}
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new AddAttachmentsCommand
|
||||
{
|
||||
PdfStream = pdfStream,
|
||||
Attachments = attachments
|
||||
};
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return PDF with attachments
|
||||
return File(resultPdf, "application/pdf", "with-attachments.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Embeds one or more files as attachments in a PDF document (supports PDF/A-3).
|
||||
/// Supports Base64-encoded PDF and attachments via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF and attachments</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF with embedded attachments</returns>
|
||||
/// <response code="200">Attachments added successfully - returns PDF</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF, or no attachments provided)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("add")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> AddAttachmentsFromBase64(
|
||||
[FromBody] AddAttachmentsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert Base64 PDF to stream
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 PDF format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Convert Base64 attachments to AttachmentFile records
|
||||
var attachments = new List<AttachmentFile>();
|
||||
foreach (var att in request.Attachments)
|
||||
{
|
||||
byte[] attBytes;
|
||||
try
|
||||
{
|
||||
attBytes = Convert.FromBase64String(att.Base64Content);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException($"Invalid Base64 format for attachment '{att.FileName}': " + ex.Message);
|
||||
}
|
||||
|
||||
attachments.Add(new AttachmentFile
|
||||
{
|
||||
FileName = att.FileName,
|
||||
Content = attBytes,
|
||||
MimeType = att.MimeType
|
||||
});
|
||||
}
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new AddAttachmentsCommand
|
||||
{
|
||||
PdfStream = pdfStream,
|
||||
Attachments = attachments
|
||||
};
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return PDF with attachments
|
||||
return File(resultPdf, "application/pdf", "with-attachments.pdf");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -182,3 +312,44 @@ public record ExtractPdfAttachmentsRequest
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF with attachments to add
|
||||
/// </summary>
|
||||
public record AddAttachmentsRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// List of attachments to embed
|
||||
/// </summary>
|
||||
public required List<AttachmentRequestDto> Attachments { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DTO for attachment file in request
|
||||
/// </summary>
|
||||
public record AttachmentRequestDto
|
||||
{
|
||||
/// <summary>
|
||||
/// File name (e.g., "invoice.xml", "document.pdf")
|
||||
/// </summary>
|
||||
/// <example>factur-x.xml</example>
|
||||
public required string FileName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// File content encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>PD94bWwgdmVyc2lvbj0iMS4wIj8+...</example>
|
||||
public required string Base64Content { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// MIME type (optional, e.g., "application/xml")
|
||||
/// </summary>
|
||||
/// <example>application/xml</example>
|
||||
public string? MimeType { get; init; }
|
||||
}
|
||||
|
||||
211
DocumentOperator.API/Controllers/PdfConversionController.cs
Normal file
211
DocumentOperator.API/Controllers/PdfConversionController.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
using DocumentService.Application.ConvertFromPdfA;
|
||||
using DocumentService.Application.ConvertToPdfA;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for PDF conversion operations (PDF ? PDF/A)
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/pdf/conversion")]
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
public class PdfConversionController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a standard PDF to PDF/A format.
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="file">The PDF file to convert</param>
|
||||
/// <param name="pdfALevel">Target PDF/A level (e.g., "PDF/A-1b", "PDF/A-2b", "PDF/A-3b")</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF/A compliant document</returns>
|
||||
/// <response code="200">PDF converted to PDF/A successfully</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF, or invalid PDF/A level)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("to-pdfa")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ConvertToPdfAFromFile(
|
||||
IFormFile file,
|
||||
[FromQuery] string pdfALevel = "PDF/A-3b",
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
throw new BadRequestException("PDF file is required");
|
||||
}
|
||||
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ConvertToPdfACommand
|
||||
{
|
||||
PdfStream = pdfStream,
|
||||
PdfALevel = pdfALevel
|
||||
};
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return PDF/A file
|
||||
return File(resultPdf, "application/pdf", "converted-pdfa.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a standard PDF to PDF/A format.
|
||||
/// Supports Base64-encoded PDF via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF and PDF/A level</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>PDF/A compliant document</returns>
|
||||
/// <response code="200">PDF converted to PDF/A successfully</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF, or invalid PDF/A level)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("to-pdfa")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ConvertToPdfAFromBase64(
|
||||
[FromBody] ConvertToPdfARequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Convert Base64 PDF to stream
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 PDF format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ConvertToPdfACommand
|
||||
{
|
||||
PdfStream = pdfStream,
|
||||
PdfALevel = request.PdfALevel ?? "PDF/A-3b"
|
||||
};
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return PDF/A file
|
||||
return File(resultPdf, "application/pdf", "converted-pdfa.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions).
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="file">The PDF/A file to convert</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Standard PDF document</returns>
|
||||
/// <response code="200">PDF/A converted to standard PDF successfully</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("from-pdfa")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ConvertFromPdfAFromFile(
|
||||
IFormFile file,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
throw new BadRequestException("PDF/A file is required");
|
||||
}
|
||||
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ConvertFromPdfACommand { PdfStream = pdfStream };
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return standard PDF file
|
||||
return File(resultPdf, "application/pdf", "converted-pdf.pdf");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions).
|
||||
/// Supports Base64-encoded PDF via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF/A</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>Standard PDF document</returns>
|
||||
/// <response code="200">PDF/A converted to standard PDF successfully</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[Obsolete("This endpoint is not implemented yet.")]
|
||||
[HttpPost("from-pdfa")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ConvertFromPdfAFromBase64(
|
||||
[FromBody] ConvertFromPdfARequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Convert Base64 PDF to stream
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 PDF format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ConvertFromPdfACommand { PdfStream = pdfStream };
|
||||
byte[] resultPdf = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return standard PDF file
|
||||
return File(resultPdf, "application/pdf", "converted-pdf.pdf");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for converting PDF to PDF/A
|
||||
/// </summary>
|
||||
public record ConvertToPdfARequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target PDF/A level (default: "PDF/A-3b")
|
||||
/// </summary>
|
||||
/// <example>PDF/A-3b</example>
|
||||
public string? PdfALevel { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for converting PDF/A to PDF
|
||||
/// </summary>
|
||||
public record ConvertFromPdfARequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF/A document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
using DocumentOperator.Application.AddAnnotation;
|
||||
using DocumentOperator.Application.AddStamp;
|
||||
using DocumentOperator.Application.MergePdfs;
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using DocumentOperator.Domain.Models.ValueObjects;
|
||||
using DocumentService.Application.AddAnnotation;
|
||||
using DocumentService.Application.AddStamp;
|
||||
using DocumentService.Application.MergePdfs;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using DocumentService.Domain.Models.ValueObjects;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentOperator.API.Controllers;
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for PDF operations (merge, stamp, annotate).
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.ValidatePdf.Queries;
|
||||
using DocumentOperator.Application.ValidatePdfA.Queries;
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.ValidatePdf.Queries;
|
||||
using DocumentService.Application.ValidatePdfA.Queries;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentOperator.API.Controllers;
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// PDF validation operations
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.SwissQrCode.Queries;
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.SwissQrCode.Queries;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentOperator.API.Controllers;
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Swiss QR Code extraction operations
|
||||
|
||||
202
DocumentOperator.API/Controllers/ZugferdController.cs
Normal file
202
DocumentOperator.API/Controllers/ZugferdController.cs
Normal file
@@ -0,0 +1,202 @@
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.ExtractZugferd;
|
||||
using DocumentService.Application.HasZugferd.Queries;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DocumentService.API.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for ZUGFeRD operations (detection, extraction)
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/pdf/zugferd")]
|
||||
[Produces("application/json")]
|
||||
public class ZugferdController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a PDF contains ZUGFeRD XML attachment.
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="file">The PDF file to check for ZUGFeRD</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD check result with metadata</returns>
|
||||
/// <response code="200">PDF successfully checked - returns ZUGFeRD status</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF, or corrupted)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("has-zugferd")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(ZugferdCheckResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> HasZugferdFromFile(
|
||||
IFormFile file,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Send query to MediatR (ValidationBehavior runs automatically)
|
||||
var query = new HasZugferdQuery { PdfStream = pdfStream };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a PDF contains ZUGFeRD XML attachment.
|
||||
/// Supports Base64-encoded PDF via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD check result with metadata</returns>
|
||||
/// <response code="200">PDF successfully checked - returns ZUGFeRD status</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF, or corrupted)</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("has-zugferd")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(ZugferdCheckResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> HasZugferdFromBase64(
|
||||
[FromBody] HasZugferdRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert Base64 to stream (wrap in try-catch to throw BadRequestException)
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Send query to MediatR (ValidationBehavior runs automatically)
|
||||
var query = new HasZugferdQuery { PdfStream = pdfStream };
|
||||
var result = await mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts ZUGFeRD XML from a PDF document.
|
||||
/// Supports multipart/form-data file upload.
|
||||
/// </summary>
|
||||
/// <param name="file">The PDF file to extract ZUGFeRD from</param>
|
||||
/// <param name="asFile">if true, 'file' (returns XML file directly); otherwise output format: 'json' (default, returns metadata + XML content)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD XML content and metadata (JSON) or XML file (application/xml)</returns>
|
||||
/// <response code="200">ZUGFeRD XML extracted successfully</response>
|
||||
/// <response code="400">Invalid input (file missing, not a PDF, or corrupted)</response>
|
||||
/// <response code="404">PDF contains no ZUGFeRD XML</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("extract")]
|
||||
[Consumes("multipart/form-data")]
|
||||
[ProducesResponseType(typeof(ZugferdExtractionResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ExtractZugferdFromFile(
|
||||
IFormFile file,
|
||||
[FromQuery] bool asFile = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Use IFormFile stream directly (no intermediate byte[] conversion)
|
||||
using var pdfStream = file.OpenReadStream();
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ExtractZugferdCommand { PdfStream = pdfStream };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return as file or JSON based on format parameter
|
||||
if (asFile)
|
||||
{
|
||||
byte[] xmlBytes = System.Text.Encoding.UTF8.GetBytes(result.XmlContent);
|
||||
return File(xmlBytes, "application/xml", result.FileName);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts ZUGFeRD XML from a PDF document.
|
||||
/// Supports Base64-encoded PDF via JSON payload.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing Base64-encoded PDF</param>
|
||||
/// <param name="format">Output format: 'json' (default, returns metadata + XML content) or 'file' (returns XML file directly)</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>ZUGFeRD XML content and metadata (JSON) or XML file (application/xml)</returns>
|
||||
/// <response code="200">ZUGFeRD XML extracted successfully</response>
|
||||
/// <response code="400">Invalid input (Base64 format error, not a PDF, or corrupted)</response>
|
||||
/// <response code="404">PDF contains no ZUGFeRD XML</response>
|
||||
/// <response code="500">Internal server error during PDF processing</response>
|
||||
[HttpPost("extract")]
|
||||
[Consumes("application/json")]
|
||||
[ProducesResponseType(typeof(ZugferdExtractionResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<IActionResult> ExtractZugferdFromBase64(
|
||||
[FromBody] ExtractZugferdRequest request,
|
||||
[FromQuery] string format = "json",
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Convert Base64 to stream (wrap in try-catch to throw BadRequestException)
|
||||
byte[] pdfBytes;
|
||||
try
|
||||
{
|
||||
pdfBytes = Convert.FromBase64String(request.Base64Pdf);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
throw new BadRequestException("Invalid Base64 format: " + ex.Message);
|
||||
}
|
||||
|
||||
using var pdfStream = new MemoryStream(pdfBytes);
|
||||
|
||||
// Send command to MediatR
|
||||
var command = new ExtractZugferdCommand { PdfStream = pdfStream };
|
||||
var result = await mediator.Send(command, cancellationToken);
|
||||
|
||||
// Return as file or JSON based on format parameter
|
||||
if (format.Equals("file", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
byte[] xmlBytes = System.Text.Encoding.UTF8.GetBytes(result.XmlContent);
|
||||
return File(xmlBytes, "application/xml", result.FileName);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF ZUGFeRD check
|
||||
/// </summary>
|
||||
public record HasZugferdRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public string Base64Pdf { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request DTO for Base64-encoded PDF ZUGFeRD extraction
|
||||
/// </summary>
|
||||
public record ExtractZugferdRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document encoded as Base64 string
|
||||
/// </summary>
|
||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||
public required string Base64Pdf { get; init; }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
@DocumentOperator.API_HostAddress = http://localhost:5028
|
||||
|
||||
GET {{DocumentOperator.API_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -32,9 +32,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DocumentOperator.Application\DocumentOperator.Application.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Domain\DocumentOperator.Domain.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Infrastructure\DocumentOperator.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Application\DocumentService.Application.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Domain\DocumentService.Domain.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Infrastructure\DocumentService.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
DocumentOperator.API/DocumentService.API.http
Normal file
6
DocumentOperator.API/DocumentService.API.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@DocumentService.API_HostAddress = http://localhost:5028
|
||||
|
||||
GET {{DocumentService.API_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
@@ -1,10 +1,10 @@
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DocumentOperator.API.Middleware;
|
||||
namespace DocumentService.API.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Central exception handling middleware
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.API.Middleware
|
||||
namespace DocumentService.API.Middleware
|
||||
{
|
||||
/// <summary>
|
||||
/// Placeholder middleware for HTTP request/response logging.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.API.Middleware
|
||||
namespace DocumentService.API.Middleware
|
||||
{
|
||||
/// <summary>
|
||||
/// Placeholder middleware for multi-tenancy resolution via X-API-Key header.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ?? DocumentOperator - Phasenplan (Feature-Driven Development)
|
||||
# ?? DocumentService - Phasenplan (Feature-Driven Development)
|
||||
|
||||
> **Stand:** 17.01.2025 | **Aktuell:** Feature 3 - ExtractAttachments ? NEXT | **Projektdauer:** 6 Wochen
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
- ? XML Comments aktiviert
|
||||
|
||||
2. **XML-Dokumentation aktiviert**
|
||||
- ? `API/DocumentOperator.API.csproj`
|
||||
- ? `API/DocumentService.API.csproj`
|
||||
- ? `<GenerateDocumentationFile>true</GenerateDocumentationFile>`
|
||||
|
||||
3. **Endpoint Dokumentation**
|
||||
@@ -146,12 +146,12 @@
|
||||
|
||||
5. **Program.cs Updates**
|
||||
- ? `builder.Services.AddSwaggerDocumentation()` statt `AddSwaggerGen()`
|
||||
- ? `using DocumentOperator.API.Configuration;` hinzugefügt
|
||||
- ? `using DocumentService.API.Configuration;` hinzugefügt
|
||||
|
||||
**Akzeptanzkriterien:**
|
||||
- ? Build erfolgreich
|
||||
- ? Alle Tests grün (11/11)
|
||||
- ? XML-Dokumentation wird generiert (`DocumentOperator.API.xml`)
|
||||
- ? XML-Dokumentation wird generiert (`DocumentService.API.xml`)
|
||||
- ? Swagger UI zeigt Endpoint `/api/v1/documents/validate` mit Dokumentation
|
||||
- ? Request/Response-Schemas sind dokumentiert
|
||||
- ? Endpoint ist im Swagger UI testbar
|
||||
|
||||
@@ -3,11 +3,12 @@ using Serilog.Ui.Core.Extensions;
|
||||
using Serilog.Ui.SqliteDataProvider.Extensions;
|
||||
using Serilog.Ui.Web.Extensions;
|
||||
using Scalar.AspNetCore;
|
||||
using DocumentOperator.Infrastructure.Configuration;
|
||||
using DocumentOperator.Application;
|
||||
using DocumentOperator.Infrastructure;
|
||||
using DocumentOperator.API.Middleware;
|
||||
using DocumentOperator.API.Configuration;
|
||||
using DocumentService.Infrastructure.Configuration;
|
||||
using DocumentService.Application;
|
||||
using DocumentService.Application.Common.Configuration;
|
||||
using DocumentService.Infrastructure;
|
||||
using DocumentService.API.Middleware;
|
||||
using DocumentService.API.Configuration;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -17,20 +18,23 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithProperty("Application", "DocumentOperator")
|
||||
.Enrich.WithProperty("Application", "DocumentService")
|
||||
.CreateLogger();
|
||||
|
||||
builder.Host.UseSerilog();
|
||||
|
||||
Log.Information("Starting DocumentOperator API...");
|
||||
Log.Information("Starting DocumentService API...");
|
||||
|
||||
try
|
||||
{
|
||||
// ========================================
|
||||
// 2. Options Pattern Configuration
|
||||
// ========================================
|
||||
builder.Services.Configure<DocumentOperatorSettings>(
|
||||
builder.Configuration.GetSection(DocumentOperatorSettings.SectionName));
|
||||
builder.Services.Configure<DocumentServiceSettings>(
|
||||
builder.Configuration.GetSection(DocumentServiceSettings.SectionName));
|
||||
|
||||
builder.Services.Configure<ZugferdSettings>(
|
||||
builder.Configuration.GetSection("ZugferdSettings"));
|
||||
|
||||
builder.Services.Configure<RedisSettings>(
|
||||
builder.Configuration.GetSection(RedisSettings.SectionName));
|
||||
@@ -44,7 +48,7 @@ try
|
||||
// ========================================
|
||||
// 3. Services (Clean Architecture Layers)
|
||||
// ========================================
|
||||
builder.Services.AddApplication(); // Application Layer (MediatR, FluentValidation, Behaviors)
|
||||
builder.Services.AddApplication(builder.Configuration); // Application Layer (MediatR, FluentValidation, Behaviors)
|
||||
builder.Services.AddInfrastructure(); // Infrastructure Layer (DevExpress, Services)
|
||||
|
||||
builder.Services.AddControllers(); // Controllers (Controller-based API)
|
||||
@@ -121,7 +125,7 @@ try
|
||||
// ========================================
|
||||
app.MapControllers(); // Maps all [ApiController] controllers
|
||||
|
||||
Log.Information("DocumentOperator API started successfully");
|
||||
Log.Information("DocumentService API started successfully");
|
||||
|
||||
app.Run();
|
||||
}
|
||||
@@ -137,7 +141,7 @@ finally
|
||||
|
||||
// Make Program class accessible for Integration Tests
|
||||
/// <summary>
|
||||
/// Entry point class for the DocumentOperator API.
|
||||
/// Entry point class for the DocumentService API.
|
||||
/// Made partial and public for integration test access.
|
||||
/// </summary>
|
||||
public partial class Program { }
|
||||
@@ -9,9 +9,9 @@
|
||||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
|
||||
<ExcludeApp_Data>false</ExcludeApp_Data>
|
||||
<ProjectGuid>c60bc965-d293-ea64-b153-1941f0648df4</ProjectGuid>
|
||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\DocumentOperator\PreRelease\API\net8\$(Version)\DocumentOperator.API.zip</DesktopBuildPackageLocation>
|
||||
<DesktopBuildPackageLocation>M:\App&Service\0 DD - Smart UP\DocumentService\PreRelease\API\net8\$(Version)\DocumentService.API.zip</DesktopBuildPackageLocation>
|
||||
<PackageAsSingleFile>true</PackageAsSingleFile>
|
||||
<DeployIisAppPath>DocumentOperator.API</DeployIisAppPath>
|
||||
<DeployIisAppPath>DocumentService.API</DeployIisAppPath>
|
||||
<_TargetId>IISWebDeployPackage</_TargetId>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DocumentOperator API - Manual Testing Guide
|
||||
# DocumentService API - Manual Testing Guide
|
||||
|
||||
This guide contains manual test scenarios for validating the DocumentOperator API endpoints using Swagger UI or tools like Postman.
|
||||
This guide contains manual test scenarios for validating the DocumentService API endpoints using Swagger UI or tools like Postman.
|
||||
|
||||
---
|
||||
|
||||
@@ -8,7 +8,7 @@ This guide contains manual test scenarios for validating the DocumentOperator AP
|
||||
|
||||
1. **Start the API:**
|
||||
```powershell
|
||||
dotnet run --project DocumentOperator.API
|
||||
dotnet run --project DocumentService.API
|
||||
```
|
||||
Default URL: `https://localhost:5001` (check console output for actual port)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
}
|
||||
},
|
||||
|
||||
"DocumentOperatorSettings": {
|
||||
"TempFolderPath": "C:\\Temp\\DocumentOperator\\Dev",
|
||||
"DocumentServiceSettings": {
|
||||
"TempFolderPath": "C:\\Temp\\DocumentService\\Dev",
|
||||
"EnableDetailedLogging": true
|
||||
},
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
"AllowedHosts": "*",
|
||||
|
||||
"Application": {
|
||||
"LogDirectory": "E:\\LogFiles\\Digital Data\\DocumentOperator.API"
|
||||
"LogDirectory": "E:\\LogFiles\\Digital Data\\DocumentService.API"
|
||||
},
|
||||
|
||||
"SwaggerSettings": {
|
||||
"EnableInProduction": true,
|
||||
"Title": "DocumentOperator API",
|
||||
"Title": "DocumentService API",
|
||||
"Version": "v1",
|
||||
"Description": "PDF document processing service using DevExpress"
|
||||
},
|
||||
@@ -45,7 +45,7 @@
|
||||
{
|
||||
"Name": "SQLite",
|
||||
"Args": {
|
||||
"sqliteDbPath": "E:\\LogFiles\\Digital Data\\DocumentOperator.API\\logs.db",
|
||||
"sqliteDbPath": "E:\\LogFiles\\Digital Data\\DocumentService.API\\logs.db",
|
||||
"tableName": "Logs",
|
||||
"storeTimestampInUtc": true
|
||||
}
|
||||
@@ -54,16 +54,32 @@
|
||||
"Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ]
|
||||
},
|
||||
|
||||
"DocumentOperatorSettings": {
|
||||
"TempFolderPath": "C:\\Temp\\DocumentOperator",
|
||||
"DocumentServiceSettings": {
|
||||
"TempFolderPath": "C:\\Temp\\DocumentService",
|
||||
"TempFileRetentionHours": 24,
|
||||
"MaxPdfSizeMB": 50,
|
||||
"EnableDetailedLogging": true
|
||||
},
|
||||
|
||||
"ZugferdSettings": {
|
||||
"ZugferdFileNames": [
|
||||
"factur-x.xml",
|
||||
"zugferd-invoice.xml",
|
||||
"ZUGFeRD-invoice.xml",
|
||||
"xrechnung.xml",
|
||||
"XRechnung.xml"
|
||||
],
|
||||
"ZugferdFileNamePatterns": [
|
||||
"factur",
|
||||
"zugferd",
|
||||
"xrechnung",
|
||||
"peppol"
|
||||
]
|
||||
},
|
||||
|
||||
"RedisSettings": {
|
||||
"ConnectionString": "localhost:6379",
|
||||
"InstanceName": "DocumentOperator:",
|
||||
"InstanceName": "DocumentService:",
|
||||
"CacheExpirationMinutes": 60
|
||||
},
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentOperator.Domain.Models.ValueObjects;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Domain.Models.ValueObjects;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentOperator.Application.AddAnnotation;
|
||||
namespace DocumentService.Application.AddAnnotation;
|
||||
|
||||
/// <summary>
|
||||
/// Command to add an annotation to a PDF document.
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentService.Application.AddAttachments;
|
||||
|
||||
/// <summary>
|
||||
/// Command to add one or more attachments to a PDF document (supports PDF/A-3)
|
||||
/// </summary>
|
||||
public record AddAttachmentsCommand : IRequest<byte[]>
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document stream. Must be positioned at the beginning (Position = 0).
|
||||
/// </summary>
|
||||
public required Stream PdfStream { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// List of attachments to embed (filename, content, optional MIME type)
|
||||
/// </summary>
|
||||
public required IReadOnlyList<AttachmentFile> Attachments { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a file to be attached to a PDF
|
||||
/// </summary>
|
||||
public record AttachmentFile
|
||||
{
|
||||
/// <summary>
|
||||
/// File name (e.g., "invoice.xml", "document.pdf")
|
||||
/// </summary>
|
||||
public required string FileName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// File content as byte array
|
||||
/// </summary>
|
||||
public required byte[] Content { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// MIME type (optional, e.g., "application/xml", "application/pdf")
|
||||
/// If not provided, will be inferred from file extension
|
||||
/// </summary>
|
||||
public string? MimeType { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for AddAttachmentsCommand
|
||||
/// </summary>
|
||||
public class AddAttachmentsCommandHandler(IPdfProcessor pdfProcessor)
|
||||
: IRequestHandler<AddAttachmentsCommand, byte[]>
|
||||
{
|
||||
public async Task<byte[]> Handle(AddAttachmentsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Convert to tuple list for IPdfProcessor
|
||||
var attachmentTuples = request.Attachments
|
||||
.Select(a => (a.FileName, a.Content, a.MimeType))
|
||||
.ToList();
|
||||
|
||||
return await pdfProcessor.AddAttachmentsAsync(request.PdfStream, attachmentTuples);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validator for AddAttachmentsCommand
|
||||
/// </summary>
|
||||
public class AddAttachmentsCommandValidator : AbstractValidator<AddAttachmentsCommand>
|
||||
{
|
||||
public AddAttachmentsCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.PdfStream)
|
||||
.NotNull()
|
||||
.WithMessage("PDF stream is required");
|
||||
|
||||
RuleFor(x => x.Attachments)
|
||||
.NotNull()
|
||||
.NotEmpty()
|
||||
.WithMessage("At least one attachment is required");
|
||||
|
||||
RuleForEach(x => x.Attachments).ChildRules(attachment =>
|
||||
{
|
||||
attachment.RuleFor(a => a.FileName)
|
||||
.NotEmpty()
|
||||
.WithMessage("Attachment file name is required");
|
||||
|
||||
attachment.RuleFor(a => a.Content)
|
||||
.NotNull()
|
||||
.NotEmpty()
|
||||
.WithMessage("Attachment content is required");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentOperator.Application.AddStamp;
|
||||
namespace DocumentService.Application.AddStamp;
|
||||
|
||||
/// <summary>
|
||||
/// Command to add a stamp (text, image, or predefined) to PDF pages.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using AutoMapper;
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentOperator.Application.CheckPdfAttachments.Queries;
|
||||
namespace DocumentService.Application.CheckPdfAttachments.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query for checking PDF attachments (Stream-based)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace DocumentOperator.Application.CheckPdfAttachments.Queries;
|
||||
namespace DocumentService.Application.CheckPdfAttachments.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for CheckPdfAttachmentsQuery
|
||||
|
||||
@@ -2,7 +2,7 @@ using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace DocumentOperator.Application.Common.Behaviors;
|
||||
namespace DocumentService.Application.Common.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// MediatR Pipeline Behavior that logs requests and tracks performance
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentOperator.Application.Common.Behaviors;
|
||||
namespace DocumentService.Application.Common.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// MediatR Pipeline Behavior that validates requests using FluentValidation
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Application.Common.DTOs;
|
||||
namespace DocumentService.Application.Common.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// DTO for attachment check result returned to API layer
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Application.Common.DTOs;
|
||||
namespace DocumentService.Application.Common.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Represents complete attachment information for a PDF document.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Application.Common.DTOs;
|
||||
namespace DocumentService.Application.Common.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Represents metadata of a single PDF attachment (embedded file).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Application.Common.DTOs;
|
||||
namespace DocumentService.Application.Common.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// PDF/A validation metadata including conformance level and validation errors/warnings
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Application.Common.DTOs;
|
||||
namespace DocumentService.Application.Common.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// PDF/A validation result including conformance level and validation errors/warnings
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Application.Common.DTOs;
|
||||
namespace DocumentService.Application.Common.DTOs;
|
||||
|
||||
public sealed class PdfMetadata(
|
||||
int pageCount,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Application.Common.DTOs;
|
||||
namespace DocumentService.Application.Common.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Response mit PDF-Metadaten
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Application.Common.DTOs;
|
||||
namespace DocumentService.Application.Common.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Swiss QR Bill data transfer object (mapped from Codecrete Bill)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Application.Common.DTOs;
|
||||
namespace DocumentService.Application.Common.DTOs;
|
||||
|
||||
/// <summary>
|
||||
/// Response containing extracted Swiss QR Code in dual format.
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
|
||||
namespace DocumentOperator.Application.Common.Interfaces;
|
||||
namespace DocumentService.Application.Common.Interfaces;
|
||||
|
||||
public interface IPdfProcessor
|
||||
{
|
||||
@@ -145,4 +145,47 @@ public interface IPdfProcessor
|
||||
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);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using Codecrete.SwissQRBill.Generator;
|
||||
|
||||
namespace DocumentOperator.Application.Common.Interfaces;
|
||||
namespace DocumentService.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for Swiss QR Code processing operations.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using AutoMapper;
|
||||
using Codecrete.SwissQRBill.Generator;
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Domain.Models.ValueObjects;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Domain.Models.ValueObjects;
|
||||
|
||||
namespace DocumentOperator.Application.Common.Mapping;
|
||||
namespace DocumentService.Application.Common.Mapping;
|
||||
|
||||
/// <summary>
|
||||
/// AutoMapper profile for mapping domain entities and external models to DTOs.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentService.Application.ConvertFromPdfA;
|
||||
|
||||
/// <summary>
|
||||
/// Command to convert a PDF/A document to a standard PDF (removes PDF/A restrictions)
|
||||
/// </summary>
|
||||
public record ConvertFromPdfACommand : IRequest<byte[]>
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF/A document stream. Must be positioned at the beginning (Position = 0).
|
||||
/// </summary>
|
||||
public required Stream PdfStream { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for ConvertFromPdfACommand
|
||||
/// </summary>
|
||||
public class ConvertFromPdfACommandHandler(IPdfProcessor pdfProcessor)
|
||||
: IRequestHandler<ConvertFromPdfACommand, byte[]>
|
||||
{
|
||||
public async Task<byte[]> Handle(ConvertFromPdfACommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await pdfProcessor.ConvertFromPdfAAsync(request.PdfStream);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validator for ConvertFromPdfACommand
|
||||
/// </summary>
|
||||
public class ConvertFromPdfACommandValidator : AbstractValidator<ConvertFromPdfACommand>
|
||||
{
|
||||
public ConvertFromPdfACommandValidator()
|
||||
{
|
||||
RuleFor(x => x.PdfStream)
|
||||
.NotNull()
|
||||
.WithMessage("PDF stream is required");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentService.Application.ConvertToPdfA;
|
||||
|
||||
/// <summary>
|
||||
/// Command to convert a standard PDF to PDF/A format
|
||||
/// </summary>
|
||||
public record ConvertToPdfACommand : IRequest<byte[]>
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document stream. Must be positioned at the beginning (Position = 0).
|
||||
/// </summary>
|
||||
public required Stream PdfStream { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Target PDF/A level (e.g., "PDF/A-1b", "PDF/A-2b", "PDF/A-3b")
|
||||
/// </summary>
|
||||
public required string PdfALevel { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for ConvertToPdfACommand
|
||||
/// </summary>
|
||||
public class ConvertToPdfACommandHandler(IPdfProcessor pdfProcessor)
|
||||
: IRequestHandler<ConvertToPdfACommand, byte[]>
|
||||
{
|
||||
public async Task<byte[]> Handle(ConvertToPdfACommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await pdfProcessor.ConvertToPdfAAsync(request.PdfStream, request.PdfALevel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validator for ConvertToPdfACommand
|
||||
/// </summary>
|
||||
public class ConvertToPdfACommandValidator : AbstractValidator<ConvertToPdfACommand>
|
||||
{
|
||||
private static readonly string[] ValidPdfALevels =
|
||||
{
|
||||
"PDF/A-1b", "PDF/A-1a",
|
||||
"PDF/A-2b", "PDF/A-2u", "PDF/A-2a",
|
||||
"PDF/A-3b", "PDF/A-3u", "PDF/A-3a"
|
||||
};
|
||||
|
||||
public ConvertToPdfACommandValidator()
|
||||
{
|
||||
RuleFor(x => x.PdfStream)
|
||||
.NotNull()
|
||||
.WithMessage("PDF stream is required");
|
||||
|
||||
RuleFor(x => x.PdfALevel)
|
||||
.NotEmpty()
|
||||
.WithMessage("PDF/A level is required")
|
||||
.Must(level => ValidPdfALevels.Contains(level, StringComparer.OrdinalIgnoreCase))
|
||||
.WithMessage($"Invalid PDF/A level. Valid values: {string.Join(", ", ValidPdfALevels)}");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DocumentOperator.Application;
|
||||
namespace DocumentService.Application;
|
||||
|
||||
/// <summary>
|
||||
/// Dependency Injection configuration for Application Layer
|
||||
@@ -11,13 +12,18 @@ public static class DependencyInjection
|
||||
/// <summary>
|
||||
/// Registers Application Layer services (MediatR, FluentValidation, AutoMapper, Behaviors)
|
||||
/// </summary>
|
||||
public static IServiceCollection AddApplication(this IServiceCollection services)
|
||||
public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var assembly = typeof(DependencyInjection).Assembly;
|
||||
|
||||
// Read LuckyPennySoft license key from appsettings.json
|
||||
var licenseKey = configuration.GetValue<string>("LuckyPennySoftLicenseKey")
|
||||
?? throw new InvalidOperationException("LuckyPennySoftLicenseKey not found in configuration");
|
||||
|
||||
// Register MediatR (scannt Assembly nach Handlers)
|
||||
services.AddMediatR(config =>
|
||||
{
|
||||
config.LicenseKey = licenseKey;
|
||||
config.RegisterServicesFromAssembly(assembly);
|
||||
|
||||
// Pipeline Behaviors (Reihenfolge wichtig!)
|
||||
@@ -29,7 +35,9 @@ public static class DependencyInjection
|
||||
services.AddValidatorsFromAssembly(assembly);
|
||||
|
||||
// Register AutoMapper (scannt Assembly nach Profiles)
|
||||
services.AddAutoMapper(cfg => { }, typeof(Common.Mapping.MappingProfile));
|
||||
services.AddAutoMapper(cfg => {
|
||||
cfg.LicenseKey = licenseKey;
|
||||
}, typeof(Common.Mapping.MappingProfile));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="MediatR" Version="14.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DocumentOperator.Domain\DocumentOperator.Domain.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Domain\DocumentService.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -1,8 +1,8 @@
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentOperator.Application.ExtractPdfAttachments;
|
||||
namespace DocumentService.Application.ExtractPdfAttachments;
|
||||
|
||||
/// <summary>
|
||||
/// Command to extract all embedded files from a PDF document and return as ZIP archive.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
using DocumentService.Application.Common.Configuration;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DocumentService.Application.ExtractZugferd;
|
||||
|
||||
/// <summary>
|
||||
/// Command to extract ZUGFeRD XML from a PDF document
|
||||
/// </summary>
|
||||
public record ExtractZugferdCommand : IRequest<ZugferdExtractionResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF document stream. Must be positioned at the beginning (Position = 0).
|
||||
/// </summary>
|
||||
public required Stream PdfStream { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for ExtractZugferdCommand.
|
||||
/// Extracts ZUGFeRD XML from PDF and returns XML content
|
||||
/// </summary>
|
||||
public class ExtractZugferdCommandHandler(
|
||||
IPdfProcessor pdfProcessor,
|
||||
IOptions<ZugferdSettings> settings)
|
||||
: IRequestHandler<ExtractZugferdCommand, ZugferdExtractionResult>
|
||||
{
|
||||
private readonly ZugferdSettings _settings = settings.Value;
|
||||
|
||||
public async Task<ZugferdExtractionResult> Handle(ExtractZugferdCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get all attachments
|
||||
var attachmentInfo = await pdfProcessor.CheckAttachmentsAsync(request.PdfStream);
|
||||
|
||||
// Find ZUGFeRD XML file using configured names and patterns
|
||||
var zugferdAttachment = attachmentInfo.Attachments.FirstOrDefault(a =>
|
||||
_settings.ZugferdFileNames.Any(name =>
|
||||
a.FileName.Equals(name, StringComparison.OrdinalIgnoreCase)) ||
|
||||
_settings.ZugferdFileNamePatterns.Any(pattern =>
|
||||
a.FileName.Contains(pattern, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (zugferdAttachment == null)
|
||||
{
|
||||
throw new NotFoundException("ZUGFeRD XML not found in PDF attachments");
|
||||
}
|
||||
|
||||
// Reset stream position for extraction
|
||||
request.PdfStream.Position = 0;
|
||||
|
||||
// Extract all attachments as ZIP
|
||||
byte[] zipBytes = await pdfProcessor.ExtractAttachmentsAsync(request.PdfStream);
|
||||
|
||||
// Find ZUGFeRD XML in ZIP
|
||||
using var zipStream = new MemoryStream(zipBytes);
|
||||
using var zipArchive = new System.IO.Compression.ZipArchive(zipStream, System.IO.Compression.ZipArchiveMode.Read);
|
||||
|
||||
var zugferdEntry = zipArchive.Entries.FirstOrDefault(e =>
|
||||
e.Name.Equals(zugferdAttachment.FileName, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (zugferdEntry == null)
|
||||
{
|
||||
throw new NotFoundException($"ZUGFeRD XML '{zugferdAttachment.FileName}' not found in extracted attachments");
|
||||
}
|
||||
|
||||
// Read XML content
|
||||
using var entryStream = zugferdEntry.Open();
|
||||
using var reader = new StreamReader(entryStream);
|
||||
string xmlContent = await reader.ReadToEndAsync(cancellationToken);
|
||||
|
||||
return new ZugferdExtractionResult
|
||||
{
|
||||
FileName = zugferdAttachment.FileName,
|
||||
XmlContent = xmlContent,
|
||||
FileSize = zugferdAttachment.SizeBytes
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validator for ExtractZugferdCommand.
|
||||
/// </summary>
|
||||
public class ExtractZugferdCommandValidator : AbstractValidator<ExtractZugferdCommand>
|
||||
{
|
||||
public ExtractZugferdCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.PdfStream)
|
||||
.NotNull()
|
||||
.WithMessage("PDF stream is required");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result DTO for ZUGFeRD extraction
|
||||
/// </summary>
|
||||
public record ZugferdExtractionResult
|
||||
{
|
||||
/// <summary>
|
||||
/// ZUGFeRD XML file name
|
||||
/// </summary>
|
||||
public required string FileName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// ZUGFeRD XML content as string
|
||||
/// </summary>
|
||||
public required string XmlContent { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// File size in bytes
|
||||
/// </summary>
|
||||
public long FileSize { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using DocumentService.Application.Common.Configuration;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace DocumentService.Application.HasZugferd.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query for checking if PDF contains ZUGFeRD XML attachment
|
||||
/// </summary>
|
||||
public record HasZugferdQuery : IRequest<ZugferdCheckResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF as stream (caller is responsible for disposal)
|
||||
/// </summary>
|
||||
public required Stream PdfStream { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handler for HasZugferdQuery
|
||||
/// Checks if PDF contains ZUGFeRD/Factur-X XML attachment
|
||||
/// </summary>
|
||||
public class HasZugferdQueryHandler(
|
||||
IPdfProcessor pdfProcessor,
|
||||
IOptions<ZugferdSettings> settings)
|
||||
: IRequestHandler<HasZugferdQuery, ZugferdCheckResult>
|
||||
{
|
||||
private readonly ZugferdSettings _settings = settings.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if PDF contains ZUGFeRD XML and returns metadata
|
||||
/// </summary>
|
||||
public async Task<ZugferdCheckResult> Handle(HasZugferdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Get all attachments
|
||||
var attachmentInfo = await pdfProcessor.CheckAttachmentsAsync(request.PdfStream);
|
||||
|
||||
// Check for ZUGFeRD/Factur-X XML files using configured names and patterns
|
||||
var zugferdAttachment = attachmentInfo.Attachments.FirstOrDefault(a =>
|
||||
_settings.ZugferdFileNames.Any(name =>
|
||||
a.FileName.Equals(name, StringComparison.OrdinalIgnoreCase)) ||
|
||||
_settings.ZugferdFileNamePatterns.Any(pattern =>
|
||||
a.FileName.Contains(pattern, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (zugferdAttachment != null)
|
||||
{
|
||||
return new ZugferdCheckResult
|
||||
{
|
||||
HasZugferd = true,
|
||||
ZugferdFileName = zugferdAttachment.FileName,
|
||||
ZugferdFileSize = zugferdAttachment.SizeBytes,
|
||||
ZugferdMimeType = zugferdAttachment.MimeType
|
||||
};
|
||||
}
|
||||
|
||||
return new ZugferdCheckResult
|
||||
{
|
||||
HasZugferd = false
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace DocumentService.Application.HasZugferd.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for HasZugferdQuery
|
||||
/// </summary>
|
||||
public class HasZugferdQueryValidator : AbstractValidator<HasZugferdQuery>
|
||||
{
|
||||
public HasZugferdQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.PdfStream)
|
||||
.NotNull()
|
||||
.WithMessage("PDF stream is required");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using FluentValidation;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentOperator.Application.MergePdfs;
|
||||
namespace DocumentService.Application.MergePdfs;
|
||||
|
||||
/// <summary>
|
||||
/// Command to merge multiple PDF documents into a single PDF.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using AutoMapper;
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentOperator.Application.SwissQrCode.Queries;
|
||||
namespace DocumentService.Application.SwissQrCode.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query for extracting Swiss QR Code from PDF (Stream-based)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using DocumentOperator.Application.SwissQrCode.Queries;
|
||||
using DocumentService.Application.SwissQrCode.Queries;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DocumentOperator.Application.SwissQrCode.Queries;
|
||||
namespace DocumentService.Application.SwissQrCode.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Validates ExtractSwissQrCodeQuery before handler execution.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using AutoMapper;
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentOperator.Application.ValidatePdf.Queries;
|
||||
namespace DocumentService.Application.ValidatePdf.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query for PDF validation (Stream-based)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using DocumentOperator.Application.ValidatePdf.Queries;
|
||||
using DocumentService.Application.ValidatePdf.Queries;
|
||||
using FluentValidation;
|
||||
|
||||
namespace DocumentOperator.Application.ValidatePdf.Queries;
|
||||
namespace DocumentService.Application.ValidatePdf.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for ValidatePdfQuery
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using AutoMapper;
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
|
||||
namespace DocumentOperator.Application.ValidatePdfA.Queries;
|
||||
namespace DocumentService.Application.ValidatePdfA.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Query for PDF/A validation (Stream-based)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace DocumentOperator.Application.ValidatePdfA.Validators;
|
||||
namespace DocumentService.Application.ValidatePdfA.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Validator for ValidatePdfAQuery
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DocumentOperator.Domain.Common.Exceptions;
|
||||
namespace DocumentService.Domain.Common.Exceptions;
|
||||
|
||||
public class BadRequestException : Exception
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Domain.Common.Exceptions;
|
||||
namespace DocumentService.Domain.Common.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Base exception for all domain-related exceptions.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Domain.Common.Exceptions;
|
||||
namespace DocumentService.Domain.Common.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when domain validation fails (e.g., invalid Value Objects).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace DocumentOperator.Domain.Common.Exceptions;
|
||||
namespace DocumentService.Domain.Common.Exceptions;
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when a requested resource is not found.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Domain.Models.ValueObjects;
|
||||
namespace DocumentService.Domain.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Coordinate origin point for PDF annotations.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Domain.Models.ValueObjects;
|
||||
namespace DocumentService.Domain.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Supported PDF annotation types
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Domain.Models.ValueObjects;
|
||||
namespace DocumentService.Domain.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Predefined stamp types with standard text and styling.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Domain.Models.ValueObjects;
|
||||
namespace DocumentService.Domain.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether the stamp should appear in the foreground (on top of content) or background (behind content).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Domain.Models.ValueObjects;
|
||||
namespace DocumentService.Domain.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the type of stamp to add to a PDF document.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Domain.Models.ValueObjects;
|
||||
namespace DocumentService.Domain.Models.ValueObjects;
|
||||
|
||||
public sealed class TenantId
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Domain.Models.ValueObjects;
|
||||
namespace DocumentService.Domain.Models.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Text markup annotation style (highlight, underline, strikeout)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Infrastructure.Configuration;
|
||||
namespace DocumentService.Infrastructure.Configuration;
|
||||
|
||||
public class ApiKeySettings
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace DocumentOperator.Infrastructure.Configuration;
|
||||
namespace DocumentService.Infrastructure.Configuration;
|
||||
|
||||
public class DocumentOperatorSettings
|
||||
public class DocumentServiceSettings
|
||||
{
|
||||
public const string SectionName = "DocumentOperatorSettings";
|
||||
public const string SectionName = "DocumentServiceSettings";
|
||||
|
||||
public string TempFolderPath { get; set; } = string.Empty;
|
||||
public int TempFileRetentionHours { get; set; }
|
||||
@@ -1,10 +1,10 @@
|
||||
namespace DocumentOperator.Infrastructure.Configuration;
|
||||
namespace DocumentService.Infrastructure.Configuration;
|
||||
|
||||
public class RedisSettings
|
||||
{
|
||||
public const string SectionName = "RedisSettings";
|
||||
|
||||
public string ConnectionString { get; set; } = "localhost:6379";
|
||||
public string InstanceName { get; set; } = "DocumentOperator:";
|
||||
public string InstanceName { get; set; } = "DocumentService:";
|
||||
public int CacheExpirationMinutes { get; set; } = 60;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace DocumentOperator.Infrastructure.Configuration;
|
||||
namespace DocumentService.Infrastructure.Configuration;
|
||||
|
||||
public class TenantInfo
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentOperator.Infrastructure.Services.PdfProcessing;
|
||||
using DocumentOperator.Infrastructure.Services.QrCodeProcessing;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Infrastructure.Services.PdfProcessing;
|
||||
using DocumentService.Infrastructure.Services.QrCodeProcessing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DocumentOperator.Infrastructure;
|
||||
namespace DocumentService.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// Dependency Injection configuration for Infrastructure Layer
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DocumentOperator.Application\DocumentOperator.Application.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Domain\DocumentOperator.Domain.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Application\DocumentService.Application.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Domain\DocumentService.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,11 +1,11 @@
|
||||
using DevExpress.Pdf;
|
||||
using DevExpress.Drawing;
|
||||
using System.Drawing;
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
|
||||
namespace DocumentOperator.Infrastructure.Services.PdfProcessing;
|
||||
namespace DocumentService.Infrastructure.Services.PdfProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// PDF processor implementation using DevExpress.Pdf library.
|
||||
@@ -1093,4 +1093,113 @@ public class DevExpressPdfProcessor : IPdfProcessor
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Add Attachments (Phase 2)
|
||||
|
||||
/// <summary>
|
||||
/// Embeds one or more files as attachments in a PDF document (supports PDF/A-3).
|
||||
/// </summary>
|
||||
public async Task<byte[]> AddAttachmentsAsync(
|
||||
Stream pdfStream,
|
||||
IReadOnlyList<(string FileName, byte[] Content, string? MimeType)> attachments)
|
||||
{
|
||||
// 1. Validate input
|
||||
ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
|
||||
ArgumentNullException.ThrowIfNull(attachments, nameof(attachments));
|
||||
|
||||
if (pdfStream.Length == 0)
|
||||
throw new BadRequestException("PDF stream cannot be empty");
|
||||
|
||||
if (pdfStream.Position != 0)
|
||||
throw new BadRequestException("PDF stream must be at position 0");
|
||||
|
||||
if (attachments.Count == 0)
|
||||
throw new BadRequestException("At least one attachment is required");
|
||||
|
||||
// TODO: Implement AddFileAttachment using DevExpress.Pdf low-level API
|
||||
// Current limitation: DevExpress.Pdf.PdfDocumentProcessor doesn't directly support adding attachments
|
||||
// Workaround options:
|
||||
// 1. Use PdfDocumentProcessor.Document to manipulate PDF structure directly (advanced)
|
||||
// 2. Use third-party library for this specific operation
|
||||
// 3. Wait for DevExpress API update
|
||||
|
||||
throw new NotImplementedException(
|
||||
"Add attachments feature is not yet implemented. " +
|
||||
"DevExpress.Pdf high-level API doesn't directly support adding file attachments. " +
|
||||
"This requires low-level PDF structure manipulation.");
|
||||
}
|
||||
|
||||
private string InferMimeType(string fileName)
|
||||
{
|
||||
string extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
return extension switch
|
||||
{
|
||||
".xml" => "application/xml",
|
||||
".pdf" => "application/pdf",
|
||||
".json" => "application/json",
|
||||
".txt" => "text/plain",
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".png" => "image/png",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PDF Conversion (Phase 3)
|
||||
|
||||
/// <summary>
|
||||
/// Converts a standard PDF to PDF/A format.
|
||||
/// </summary>
|
||||
public async Task<byte[]> ConvertToPdfAAsync(Stream pdfStream, string pdfALevel)
|
||||
{
|
||||
// 1. Validate input
|
||||
ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
|
||||
|
||||
if (pdfStream.Length == 0)
|
||||
throw new BadRequestException("PDF stream cannot be empty");
|
||||
|
||||
if (pdfStream.Position != 0)
|
||||
throw new BadRequestException("PDF stream must be at position 0");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pdfALevel))
|
||||
throw new BadRequestException("PDF/A level is required");
|
||||
|
||||
// TODO: Implement PDF to PDF/A conversion using DevExpress
|
||||
// Current limitation: DevExpress.Pdf.PdfDocumentProcessor doesn't directly support PDF/A conversion
|
||||
// Requires using specialized PDF/A conversion libraries or low-level PDF manipulation
|
||||
|
||||
throw new NotImplementedException(
|
||||
$"PDF to PDF/A conversion ({pdfALevel}) is not yet implemented. " +
|
||||
"DevExpress.Pdf high-level API doesn't directly support PDF/A conversion. " +
|
||||
"This requires specialized PDF/A conversion logic.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PDF/A document to a standard PDF (removes PDF/A restrictions).
|
||||
/// </summary>
|
||||
public async Task<byte[]> ConvertFromPdfAAsync(Stream pdfStream)
|
||||
{
|
||||
// 1. Validate input
|
||||
ArgumentNullException.ThrowIfNull(pdfStream, nameof(pdfStream));
|
||||
|
||||
if (pdfStream.Length == 0)
|
||||
throw new BadRequestException("PDF stream cannot be empty");
|
||||
|
||||
if (pdfStream.Position != 0)
|
||||
throw new BadRequestException("PDF stream must be at position 0");
|
||||
|
||||
// 2. Load PDF/A
|
||||
using var processor = new PdfDocumentProcessor();
|
||||
processor.LoadDocument(pdfStream);
|
||||
|
||||
// 3. Save as standard PDF
|
||||
// DevExpress SaveDocument without special options creates standard PDF
|
||||
using var outputStream = new MemoryStream();
|
||||
processor.SaveDocument(outputStream);
|
||||
|
||||
return await Task.FromResult(outputStream.ToArray());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
using Codecrete.SwissQRBill.Generator;
|
||||
using DevExpress.Drawing;
|
||||
using DevExpress.Pdf;
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.QrCode;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace DocumentOperator.Infrastructure.Services.QrCodeProcessing;
|
||||
namespace DocumentService.Infrastructure.Services.QrCodeProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// Swiss QR Code processor using DevExpress PDF API for image extraction,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace DocumentOperator.Infrastructure.Services;
|
||||
namespace DocumentService.Infrastructure.Services;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DocumentOperator.API\DocumentOperator.API.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Domain\DocumentOperator.Domain.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Application\DocumentOperator.Application.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Infrastructure\DocumentOperator.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.API\DocumentService.API.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Domain\DocumentService.Domain.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Application\DocumentService.Application.csproj" />
|
||||
<ProjectReference Include="..\DocumentOperator.Infrastructure\DocumentService.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,5 +1,5 @@
|
||||
using DocumentOperator.API.Controllers; // For ExtractSwissQrCodeBase64Request DTO
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentService.API.Controllers; // For ExtractSwissQrCodeBase64Request DTO
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using System.Net;
|
||||
@@ -7,7 +7,7 @@ using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace DocumentOperator.Tests.Integration.API;
|
||||
namespace DocumentService.Tests.Integration.API;
|
||||
|
||||
public sealed class ExtractSwissQrCodeEndpointTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
@@ -27,7 +27,7 @@ public sealed class ExtractSwissQrCodeEndpointTests : IClassFixture<WebApplicati
|
||||
public async Task POST_ExtractSwissQrCode_ValidRequest_Returns200()
|
||||
{
|
||||
// Arrange
|
||||
var validPdfBase64 = GetEmbeddedResourceAsBase64("DocumentOperator.Tests.TestData.Pdfs.valid.pdf");
|
||||
var validPdfBase64 = GetEmbeddedResourceAsBase64("DocumentService.Tests.TestData.Pdfs.valid.pdf");
|
||||
|
||||
var request = new ExtractSwissQrCodeBase64Request
|
||||
{
|
||||
@@ -77,7 +77,7 @@ public sealed class ExtractSwissQrCodeEndpointTests : IClassFixture<WebApplicati
|
||||
{
|
||||
// Arrange: References are OPTIONAL - null should not cause validation error (400)
|
||||
// Using pdfWithSwissQRCode.pdf which actually has a QR code, so we get 200
|
||||
var validPdfBase64 = GetEmbeddedResourceAsBase64("DocumentOperator.Tests.TestData.Pdfs.pdfWithSwissQRCode.pdf");
|
||||
var validPdfBase64 = GetEmbeddedResourceAsBase64("DocumentService.Tests.TestData.Pdfs.pdfWithSwissQRCode.pdf");
|
||||
|
||||
var request = new
|
||||
{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using DocumentOperator.API.Controllers; // For CheckPdfAttachmentsRequest DTO
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentService.API.Controllers; // For CheckPdfAttachmentsRequest DTO
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace DocumentOperator.Tests.Integration.API;
|
||||
namespace DocumentService.Tests.Integration.API;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for PdfAttachmentController.
|
||||
@@ -31,7 +31,7 @@ public class PdfAttachmentControllerTests : IClassFixture<WebApplicationFactory<
|
||||
private static async Task<byte[]> LoadTestPdfAsync(string filename)
|
||||
{
|
||||
var assembly = typeof(PdfAttachmentControllerTests).Assembly;
|
||||
var resourceName = $"DocumentOperator.Tests.TestData.Pdfs.{filename}";
|
||||
var resourceName = $"DocumentService.Tests.TestData.Pdfs.{filename}";
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null)
|
||||
|
||||
@@ -3,12 +3,12 @@ using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using DocumentOperator.API.Controllers;
|
||||
using DocumentOperator.Domain.Models.ValueObjects;
|
||||
using DocumentService.API.Controllers;
|
||||
using DocumentService.Domain.Models.ValueObjects;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
|
||||
namespace DocumentOperator.Tests.Integration.API;
|
||||
namespace DocumentService.Tests.Integration.API;
|
||||
|
||||
public class PdfOperationsControllerTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
@@ -22,7 +22,7 @@ public class PdfOperationsControllerTests : IClassFixture<WebApplicationFactory<
|
||||
private static Stream LoadTestPdfAsStream(string fileName)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = $"DocumentOperator.Tests.TestData.Pdfs.{fileName}";
|
||||
var resourceName = $"DocumentService.Tests.TestData.Pdfs.{fileName}";
|
||||
return assembly.GetManifestResourceStream(resourceName)
|
||||
?? throw new FileNotFoundException($"Embedded resource not found: {resourceName}");
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using DocumentOperator.API.Controllers; // For Base64Request DTOs
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentService.API.Controllers; // For Base64Request DTOs
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using FluentAssertions;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace DocumentOperator.Tests.Integration.API;
|
||||
namespace DocumentService.Tests.Integration.API;
|
||||
|
||||
public class PdfValidationControllerTests : IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
@@ -27,7 +27,7 @@ public class PdfValidationControllerTests : IClassFixture<WebApplicationFactory<
|
||||
// Arrange
|
||||
// Verwende ein echtes Test-PDF (embedded resource aus Unit Tests)
|
||||
var assembly = typeof(PdfValidationControllerTests).Assembly;
|
||||
var resourceName = "DocumentOperator.Tests.TestData.Pdfs.valid.pdf";
|
||||
var resourceName = "DocumentService.Tests.TestData.Pdfs.valid.pdf";
|
||||
|
||||
byte[] pdfBytes;
|
||||
using (var stream = assembly.GetManifestResourceStream(resourceName))
|
||||
@@ -101,7 +101,7 @@ public class PdfValidationControllerTests : IClassFixture<WebApplicationFactory<
|
||||
{
|
||||
// Arrange
|
||||
var assembly = typeof(PdfValidationControllerTests).Assembly;
|
||||
var resourceName = "DocumentOperator.Tests.TestData.Pdfs.valid.pdf";
|
||||
var resourceName = "DocumentService.Tests.TestData.Pdfs.valid.pdf";
|
||||
|
||||
byte[] pdfBytes;
|
||||
using (var stream = assembly.GetManifestResourceStream(resourceName))
|
||||
@@ -176,7 +176,7 @@ public class PdfValidationControllerTests : IClassFixture<WebApplicationFactory<
|
||||
{
|
||||
// Arrange
|
||||
var assembly = typeof(PdfValidationControllerTests).Assembly;
|
||||
var resourceName = "DocumentOperator.Tests.TestData.Pdfs.valid.pdf";
|
||||
var resourceName = "DocumentService.Tests.TestData.Pdfs.valid.pdf";
|
||||
|
||||
byte[] pdfBytes;
|
||||
using (var stream = assembly.GetManifestResourceStream(resourceName))
|
||||
@@ -250,7 +250,7 @@ public class PdfValidationControllerTests : IClassFixture<WebApplicationFactory<
|
||||
{
|
||||
// Arrange
|
||||
var assembly = typeof(PdfValidationControllerTests).Assembly;
|
||||
var resourceName = "DocumentOperator.Tests.TestData.Pdfs.valid.pdf";
|
||||
var resourceName = "DocumentService.Tests.TestData.Pdfs.valid.pdf";
|
||||
|
||||
byte[] pdfBytes;
|
||||
using (var stream = assembly.GetManifestResourceStream(resourceName))
|
||||
|
||||
BIN
DocumentOperator.Tests/TestData/Pdfs/ZUGFeRD-Example.pdf
Normal file
BIN
DocumentOperator.Tests/TestData/Pdfs/ZUGFeRD-Example.pdf
Normal file
Binary file not shown.
152
DocumentOperator.Tests/TestData/Pdfs/attachment.xml
Normal file
152
DocumentOperator.Tests/TestData/Pdfs/attachment.xml
Normal file
@@ -0,0 +1,152 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100" xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100" xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100 ../../../schemas/UN_CEFACT/CrossIndustryInvoice_100pD16B.xsd">
|
||||
<rsm:ExchangedDocumentContext>
|
||||
<ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
|
||||
</ram:GuidelineSpecifiedDocumentContextParameter>
|
||||
</rsm:ExchangedDocumentContext>
|
||||
<rsm:ExchangedDocument>
|
||||
<ram:ID>2021_10</ram:ID>
|
||||
<ram:TypeCode>380</ram:TypeCode>
|
||||
<ram:IssueDateTime>
|
||||
<udt:DateTimeString format="102">20210924</udt:DateTimeString>
|
||||
</ram:IssueDateTime>
|
||||
</rsm:ExchangedDocument>
|
||||
<rsm:SupplyChainTradeTransaction>
|
||||
<ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:AssociatedDocumentLineDocument>
|
||||
<ram:LineID>1</ram:LineID>
|
||||
</ram:AssociatedDocumentLineDocument>
|
||||
<ram:SpecifiedTradeProduct>
|
||||
<ram:Name>Project management</ram:Name>
|
||||
<ram:Description/>
|
||||
</ram:SpecifiedTradeProduct>
|
||||
<ram:SpecifiedLineTradeAgreement>
|
||||
<ram:NetPriceProductTradePrice>
|
||||
<ram:ChargeAmount>500.000000</ram:ChargeAmount>
|
||||
</ram:NetPriceProductTradePrice>
|
||||
</ram:SpecifiedLineTradeAgreement>
|
||||
<ram:SpecifiedLineTradeDelivery>
|
||||
<ram:BilledQuantity unitCode="C62">2.00</ram:BilledQuantity>
|
||||
</ram:SpecifiedLineTradeDelivery>
|
||||
<ram:SpecifiedLineTradeSettlement>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
<ram:LineTotalAmount>1000.00</ram:LineTotalAmount>
|
||||
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
</ram:SpecifiedLineTradeSettlement>
|
||||
</ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:AssociatedDocumentLineDocument>
|
||||
<ram:LineID>2</ram:LineID>
|
||||
</ram:AssociatedDocumentLineDocument>
|
||||
<ram:SpecifiedTradeProduct>
|
||||
<ram:Name>Consulting</ram:Name>
|
||||
<ram:Description/>
|
||||
</ram:SpecifiedTradeProduct>
|
||||
<ram:SpecifiedLineTradeAgreement>
|
||||
<ram:NetPriceProductTradePrice>
|
||||
<ram:ChargeAmount>40.000000</ram:ChargeAmount>
|
||||
</ram:NetPriceProductTradePrice>
|
||||
</ram:SpecifiedLineTradeAgreement>
|
||||
<ram:SpecifiedLineTradeDelivery>
|
||||
<ram:BilledQuantity unitCode="C62">5.00</ram:BilledQuantity>
|
||||
</ram:SpecifiedLineTradeDelivery>
|
||||
<ram:SpecifiedLineTradeSettlement>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
<ram:LineTotalAmount>200.00</ram:LineTotalAmount>
|
||||
</ram:SpecifiedTradeSettlementLineMonetarySummation>
|
||||
</ram:SpecifiedLineTradeSettlement>
|
||||
</ram:IncludedSupplyChainTradeLineItem>
|
||||
<ram:ApplicableHeaderTradeAgreement>
|
||||
<ram:BuyerReference>139877</ram:BuyerReference>
|
||||
<ram:SellerTradeParty>
|
||||
<ram:Name>Webware Internet Solutions GmbH</ram:Name>
|
||||
<ram:SpecifiedLegalOrganization>
|
||||
<ram:ID>HRB 15635</ram:ID>
|
||||
</ram:SpecifiedLegalOrganization>
|
||||
<ram:DefinedTradeContact>
|
||||
<ram:PersonName>John Doe</ram:PersonName>
|
||||
<ram:TelephoneUniversalCommunication>
|
||||
<ram:CompleteNumber>+49(0)561-560123456</ram:CompleteNumber>
|
||||
</ram:TelephoneUniversalCommunication>
|
||||
<ram:EmailURIUniversalCommunication>
|
||||
<ram:URIID>johndoe@webware24.de</ram:URIID>
|
||||
</ram:EmailURIUniversalCommunication>
|
||||
</ram:DefinedTradeContact>
|
||||
<ram:PostalTradeAddress>
|
||||
<ram:PostcodeCode>34130</ram:PostcodeCode>
|
||||
<ram:LineOne>Teichstr. 14-16</ram:LineOne>
|
||||
<ram:CityName>Kassel</ram:CityName>
|
||||
<ram:CountryID>DE</ram:CountryID>
|
||||
</ram:PostalTradeAddress>
|
||||
<ram:URIUniversalCommunication>
|
||||
<ram:URIID schemeID="9930">DE279247134</ram:URIID>
|
||||
</ram:URIUniversalCommunication>
|
||||
<ram:SpecifiedTaxRegistration>
|
||||
<ram:ID schemeID="FC">262/481/0918</ram:ID>
|
||||
</ram:SpecifiedTaxRegistration>
|
||||
<ram:SpecifiedTaxRegistration>
|
||||
<ram:ID schemeID="VA">DE279247134</ram:ID>
|
||||
</ram:SpecifiedTaxRegistration>
|
||||
</ram:SellerTradeParty>
|
||||
<ram:BuyerTradeParty>
|
||||
<ram:Name>Agoratech</ram:Name>
|
||||
<ram:PostalTradeAddress>
|
||||
<ram:PostcodeCode>34130</ram:PostcodeCode>
|
||||
<ram:LineOne>Teichstr. 14-16</ram:LineOne>
|
||||
<ram:CityName>Kassel</ram:CityName>
|
||||
<ram:CountryID>DE</ram:CountryID>
|
||||
</ram:PostalTradeAddress>
|
||||
<ram:URIUniversalCommunication>
|
||||
<ram:URIID schemeID="9930">DE319642369</ram:URIID>
|
||||
</ram:URIUniversalCommunication>
|
||||
</ram:BuyerTradeParty>
|
||||
</ram:ApplicableHeaderTradeAgreement>
|
||||
<ram:ApplicableHeaderTradeDelivery>
|
||||
<ram:ActualDeliverySupplyChainEvent>
|
||||
<ram:OccurrenceDateTime>
|
||||
<udt:DateTimeString format="102">20211101</udt:DateTimeString>
|
||||
</ram:OccurrenceDateTime>
|
||||
</ram:ActualDeliverySupplyChainEvent>
|
||||
</ram:ApplicableHeaderTradeDelivery>
|
||||
<ram:ApplicableHeaderTradeSettlement>
|
||||
<ram:InvoiceCurrencyCode>EUR</ram:InvoiceCurrencyCode>
|
||||
<ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
<ram:TypeCode>42</ram:TypeCode>
|
||||
<ram:PayeePartyCreditorFinancialAccount>
|
||||
<ram:IBANID/>
|
||||
</ram:PayeePartyCreditorFinancialAccount>
|
||||
<ram:PayeeSpecifiedCreditorFinancialInstitution>
|
||||
<ram:BICID/>
|
||||
</ram:PayeeSpecifiedCreditorFinancialInstitution>
|
||||
</ram:SpecifiedTradeSettlementPaymentMeans>
|
||||
<ram:ApplicableTradeTax>
|
||||
<ram:CalculatedAmount>228</ram:CalculatedAmount>
|
||||
<ram:TypeCode>VAT</ram:TypeCode>
|
||||
<ram:BasisAmount>1200</ram:BasisAmount>
|
||||
<ram:CategoryCode>S</ram:CategoryCode>
|
||||
<ram:RateApplicablePercent>19.00</ram:RateApplicablePercent>
|
||||
</ram:ApplicableTradeTax>
|
||||
<ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||
<ram:LineTotalAmount>1200</ram:LineTotalAmount>
|
||||
<ram:ChargeTotalAmount>0</ram:ChargeTotalAmount>
|
||||
<ram:AllowanceTotalAmount>0</ram:AllowanceTotalAmount>
|
||||
<ram:TaxBasisTotalAmount>1200.00</ram:TaxBasisTotalAmount>
|
||||
<ram:TaxTotalAmount currencyID="EUR">228.00</ram:TaxTotalAmount>
|
||||
<ram:GrandTotalAmount>1428.00</ram:GrandTotalAmount>
|
||||
<ram:TotalPrepaidAmount>0.00</ram:TotalPrepaidAmount>
|
||||
<ram:DuePayableAmount>1428.00</ram:DuePayableAmount>
|
||||
</ram:SpecifiedTradeSettlementHeaderMonetarySummation>
|
||||
</ram:ApplicableHeaderTradeSettlement>
|
||||
</rsm:SupplyChainTradeTransaction>
|
||||
</rsm:CrossIndustryInvoice>
|
||||
BIN
DocumentOperator.Tests/TestData/Pdfs/withoutAttachment.pdf
Normal file
BIN
DocumentOperator.Tests/TestData/Pdfs/withoutAttachment.pdf
Normal file
Binary file not shown.
@@ -1,12 +1,12 @@
|
||||
using AutoMapper;
|
||||
using DocumentOperator.Application.CheckPdfAttachments.Queries;
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentService.Application.CheckPdfAttachments.Queries;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
|
||||
namespace DocumentOperator.Tests.Unit.Application.CheckPdfAttachments;
|
||||
namespace DocumentService.Tests.Unit.Application.CheckPdfAttachments;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for CheckPdfAttachmentsQueryHandler.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
using AutoMapper;
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentOperator.Application.ValidatePdf.Queries;
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Application.ValidatePdf.Queries;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace DocumentOperator.Tests.Unit.Application.Features.ValidatePdf;
|
||||
namespace DocumentService.Tests.Unit.Application.Features.ValidatePdf;
|
||||
|
||||
public class ValidatePdfHandlerTests
|
||||
{
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using AutoMapper;
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentOperator.Application.ValidatePdfA.Queries;
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Application.ValidatePdfA.Queries;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace DocumentOperator.Tests.Unit.Application.Features.ValidatePdfA;
|
||||
namespace DocumentService.Tests.Unit.Application.Features.ValidatePdfA;
|
||||
|
||||
public class ValidatePdfAQueryHandlerTests
|
||||
{
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Reflection;
|
||||
using DocumentOperator.Application.Common.DTOs;
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using DocumentOperator.Infrastructure.Services.PdfProcessing;
|
||||
using DocumentService.Application.Common.DTOs;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using DocumentService.Infrastructure.Services.PdfProcessing;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace DocumentOperator.Tests.Unit.Infrastructure.Services.PdfProcessing;
|
||||
namespace DocumentService.Tests.Unit.Infrastructure.Services.PdfProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for DevExpressPdfProcessor.
|
||||
@@ -31,7 +31,7 @@ public class DevExpressPdfProcessorTests
|
||||
private static byte[] LoadTestPdf(string filename)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = $"DocumentOperator.Tests.TestData.Pdfs.{filename}";
|
||||
var resourceName = $"DocumentService.Tests.TestData.Pdfs.{filename}";
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Reflection;
|
||||
using DocumentOperator.Application.Common.Interfaces;
|
||||
using DocumentOperator.Domain.Common.Exceptions;
|
||||
using DocumentOperator.Infrastructure.Services.QrCodeProcessing;
|
||||
using DocumentService.Application.Common.Interfaces;
|
||||
using DocumentService.Domain.Common.Exceptions;
|
||||
using DocumentService.Infrastructure.Services.QrCodeProcessing;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace DocumentOperator.Tests.Unit.Infrastructure.Services.QrCodeProcessing;
|
||||
namespace DocumentService.Tests.Unit.Infrastructure.Services.QrCodeProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for DevExpressSwissQrCodeProcessor.
|
||||
@@ -30,7 +30,7 @@ public class DevExpressSwissQrCodeProcessorTests
|
||||
private static Stream LoadTestPdf(string filename)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceName = $"DocumentOperator.Tests.TestData.Pdfs.{filename}";
|
||||
var resourceName = $"DocumentService.Tests.TestData.Pdfs.{filename}";
|
||||
|
||||
var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
|
||||
|
||||
@@ -3,15 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.37328.6
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentOperator.API", "DocumentOperator.API\DocumentOperator.API.csproj", "{BA41F6A2-EE29-48F9-A3CB-29A05E57CF30}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.API", "DocumentOperator.API\DocumentService.API.csproj", "{BA41F6A2-EE29-48F9-A3CB-29A05E57CF30}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentOperator.Application", "DocumentOperator.Application\DocumentOperator.Application.csproj", "{B2B735FC-5F39-4CFF-9C54-C4F8820880B6}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Application", "DocumentOperator.Application\DocumentService.Application.csproj", "{B2B735FC-5F39-4CFF-9C54-C4F8820880B6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentOperator.Infrastructure", "DocumentOperator.Infrastructure\DocumentOperator.Infrastructure.csproj", "{C4CA19A8-8168-453B-B4ED-77E032CFE52E}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Infrastructure", "DocumentOperator.Infrastructure\DocumentService.Infrastructure.csproj", "{C4CA19A8-8168-453B-B4ED-77E032CFE52E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentOperator.Domain", "DocumentOperator.Domain\DocumentOperator.Domain.csproj", "{B4C1C3ED-D3E8-4272-8704-2E73CEA6A0DD}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Domain", "DocumentOperator.Domain\DocumentService.Domain.csproj", "{B4C1C3ED-D3E8-4272-8704-2E73CEA6A0DD}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentOperator.Tests", "DocumentOperator.Tests\DocumentOperator.Tests.csproj", "{32D2E997-3DA7-4061-8A50-DBB34BBC3E5A}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentService.Tests", "DocumentOperator.Tests\DocumentService.Tests.csproj", "{32D2E997-3DA7-4061-8A50-DBB34BBC3E5A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3F9E8D8E-1234-4567-89AB-CDEF01234567}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
@@ -28,6 +28,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fake-pfd", "fake-pfd", "{02
|
||||
fake-pdf\with-image.pdf = fake-pdf\with-image.pdf
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "core", "core", "{0B80DDC4-A13C-4C5F-8520-AA2F3650E884}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "presentation", "presentation", "{62E8F4B3-EA49-41D2-8843-0636EB4421BB}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infrastructure", "infrastructure", "{9BACC18A-5C6D-4678-A43C-A19907AE9CF3}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2B18D2F8-40C1-4CA0-99AB-7F98F254FEB8}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{D64772B8-6213-4031-8064-2567F1E724F6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -59,7 +69,15 @@ Global
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{BA41F6A2-EE29-48F9-A3CB-29A05E57CF30} = {62E8F4B3-EA49-41D2-8843-0636EB4421BB}
|
||||
{B2B735FC-5F39-4CFF-9C54-C4F8820880B6} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
|
||||
{C4CA19A8-8168-453B-B4ED-77E032CFE52E} = {9BACC18A-5C6D-4678-A43C-A19907AE9CF3}
|
||||
{B4C1C3ED-D3E8-4272-8704-2E73CEA6A0DD} = {0B80DDC4-A13C-4C5F-8520-AA2F3650E884}
|
||||
{32D2E997-3DA7-4061-8A50-DBB34BBC3E5A} = {D64772B8-6213-4031-8064-2567F1E724F6}
|
||||
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {3F9E8D8E-1234-4567-89AB-CDEF01234567}
|
||||
{0B80DDC4-A13C-4C5F-8520-AA2F3650E884} = {2B18D2F8-40C1-4CA0-99AB-7F98F254FEB8}
|
||||
{62E8F4B3-EA49-41D2-8843-0636EB4421BB} = {2B18D2F8-40C1-4CA0-99AB-7F98F254FEB8}
|
||||
{9BACC18A-5C6D-4678-A43C-A19907AE9CF3} = {2B18D2F8-40C1-4CA0-99AB-7F98F254FEB8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {832CA90A-06D6-4312-9B35-16CC665EB37C}
|
||||
@@ -1,4 +1,4 @@
|
||||
# DocumentOperator - Required Functions and Features
|
||||
# DocumentService - Required Functions and Features
|
||||
|
||||
**Project:** DocumentService (DOC)
|
||||
**Ticket:** DOC-1 - GDPicture and Nutrient Replacing
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes in detail all PDF processing functions that the DocumentOperator service must implement. These functions will be implemented using the DevExpress Office File API as a replacement for the GDPicture and Nutrient libraries.
|
||||
This document describes in detail all PDF processing functions that the DocumentService service must implement. These functions will be implemented using the DevExpress Office File API as a replacement for the GDPicture and Nutrient libraries.
|
||||
|
||||
---
|
||||
|
||||
@@ -548,7 +548,7 @@ Body: {
|
||||
- **DevExpress PDF API Documentation:** https://docs.devexpress.com/OfficeFileAPI/
|
||||
- **PDF/A Standard:** ISO 19005
|
||||
- **ZUGFeRD Standard:** https://www.ferd-net.de/
|
||||
- **Ticket:** DOC-1 (M:\Austausch\DocumentOperator)
|
||||
- **Ticket:** DOC-1 (M:\Austausch\DocumentService)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user