Add PDF attachment and PDF/A conversion features
Introduced endpoints for embedding attachments in PDFs and converting between standard PDFs and PDF/A formats. Added `PdfAttachmentController` and `PdfConversionController` with multipart/form-data and Base64-based support. Implemented commands, handlers, and validators for these operations. Extended `IPdfProcessor` with methods for adding attachments and PDF/A conversion. Partially implemented functionality in `DevExpressPdfProcessor`, including `ConvertFromPdfAAsync`. Added `attachment.xml` and `withoutAttachment.pdf` as resources for testing. Marked endpoints as `[Obsolete]` to indicate incomplete implementation. Improved validation and error handling for commands.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using DocumentOperator.Application.AddAttachments;
|
||||||
using DocumentOperator.Application.CheckPdfAttachments.Queries;
|
using DocumentOperator.Application.CheckPdfAttachments.Queries;
|
||||||
using DocumentOperator.Application.Common.DTOs;
|
using DocumentOperator.Application.Common.DTOs;
|
||||||
using DocumentOperator.Application.ExtractPdfAttachments;
|
using DocumentOperator.Application.ExtractPdfAttachments;
|
||||||
@@ -157,6 +158,135 @@ public class PdfAttachmentController(IMediator mediator) : ControllerBase
|
|||||||
// Return ZIP file
|
// Return ZIP file
|
||||||
return File(zipBytes, "application/zip", "attachments.zip");
|
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>
|
/// <summary>
|
||||||
@@ -182,3 +312,44 @@ public record ExtractPdfAttachmentsRequest
|
|||||||
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
/// <example>JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c...</example>
|
||||||
public required string Base64Pdf { get; init; }
|
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 DocumentOperator.Application.ConvertFromPdfA;
|
||||||
|
using DocumentOperator.Application.ConvertToPdfA;
|
||||||
|
using DocumentOperator.Domain.Common.Exceptions;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace DocumentOperator.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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using DocumentOperator.Application.Common.Interfaces;
|
||||||
|
using FluentValidation;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DocumentOperator.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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -145,4 +145,47 @@ public interface IPdfProcessor
|
|||||||
Domain.Models.ValueObjects.StampPlacement placement = Domain.Models.ValueObjects.StampPlacement.Foreground,
|
Domain.Models.ValueObjects.StampPlacement placement = Domain.Models.ValueObjects.StampPlacement.Foreground,
|
||||||
byte[]? imageBytes = null,
|
byte[]? imageBytes = null,
|
||||||
Domain.Models.ValueObjects.PredefinedStampType? predefinedType = 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);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using DocumentOperator.Application.Common.Interfaces;
|
||||||
|
using FluentValidation;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DocumentOperator.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 DocumentOperator.Application.Common.Interfaces;
|
||||||
|
using FluentValidation;
|
||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace DocumentOperator.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)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1093,4 +1093,113 @@ public class DevExpressPdfProcessor : IPdfProcessor
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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
|
||||||
}
|
}
|
||||||
|
|||||||
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.
Reference in New Issue
Block a user