Add project files.

This commit is contained in:
OlgunR
2026-05-21 14:35:02 +02:00
parent b315aead20
commit dc551c2313
106 changed files with 303666 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
namespace DXApp.TemplateKitProject.Services
{
public class EmailInvoiceService
{
}
}

View File

@@ -0,0 +1,142 @@
using DevExpress.Pdf;
using DXApp.TemplateKitProject.Models;
namespace DXApp.TemplateKitProject.Services;
public class PdfAttachmentExtractorService(
IConfiguration configuration,
ILogger<PdfAttachmentExtractorService> logger)
{
private static readonly string[] ZugferdFileNames =
[
"zugferd-invoice.xml",
"factur-x.xml",
"xrechnung.xml",
"zugferd_2p0_en16931_muster.xml",
"cii-data.xml"
];
public PdfExtractionResult ExtractAttachments(Stream pdfStream, string sourceFileName)
{
var result = new PdfExtractionResult();
var outputDir = ResolveOutputDirectory(sourceFileName);
try
{
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(pdfStream);
// Fix: .ToList() → IEnumerable → List<T> mit Count-Property
var attachments = processor.Document.FileAttachments.ToList();
if (attachments.Count == 0)
{
logger.LogInformation("PDF '{FileName}': Keine Anhänge gefunden.", sourceFileName);
return result;
}
logger.LogInformation(
"PDF '{FileName}': {Count} Anhang/Anhänge gefunden.",
sourceFileName, attachments.Count);
Directory.CreateDirectory(outputDir);
foreach (var attachment in attachments)
{
var extracted = SaveAttachment(attachment, outputDir);
if (extracted is not null)
result.Attachments.Add(extracted);
}
}
catch (Exception ex)
{
logger.LogError(ex,
"Fehler beim Extrahieren der Anhänge aus '{FileName}'.", sourceFileName);
throw;
}
LogExtractionSummary(sourceFileName, result);
return result;
}
private ExtractedAttachment? SaveAttachment(PdfFileAttachment attachment, string outputDir)
{
try
{
var safeFileName = SanitizeFileName(attachment.FileName);
var targetPath = EnsureUniqueFilePath(Path.Combine(outputDir, safeFileName));
var data = attachment.Data;
File.WriteAllBytes(targetPath, data);
var isZugferd = IsZugferdXml(attachment.FileName);
logger.LogInformation(
" → Gespeichert: '{FileName}' ({Bytes} Bytes){Zugferd}",
safeFileName, data.Length,
isZugferd ? " [ZUGFeRD/Factur-X XML]" : string.Empty);
return new ExtractedAttachment
{
OriginalFileName = attachment.FileName,
SavedFilePath = targetPath,
FileSizeBytes = data.Length,
IsZugferdXml = isZugferd
};
}
catch (Exception ex)
{
logger.LogWarning(ex,
" → Anhang '{Name}' konnte nicht gespeichert werden.", attachment.FileName);
return null;
}
}
private string ResolveOutputDirectory(string sourceFileName)
{
var baseDir = configuration["PdfExtraction:OutputDirectory"]
?? Path.Combine(Path.GetTempPath(), "PdfExtractions");
var folderName = $"{Path.GetFileNameWithoutExtension(sourceFileName)}_{DateTime.UtcNow:yyyyMMdd_HHmmss}";
return Path.Combine(baseDir, folderName);
}
private static bool IsZugferdXml(string fileName)
{
var lower = fileName.ToLowerInvariant();
return ZugferdFileNames.Any(z => lower.EndsWith(z, StringComparison.OrdinalIgnoreCase));
}
private static string SanitizeFileName(string fileName)
{
var invalid = Path.GetInvalidFileNameChars();
var safe = string.Concat(fileName.Select(c => invalid.Contains(c) ? '_' : c));
return string.IsNullOrWhiteSpace(safe) ? "attachment" : safe;
}
private static string EnsureUniqueFilePath(string filePath)
{
if (!File.Exists(filePath)) return filePath;
var dir = Path.GetDirectoryName(filePath)!;
var name = Path.GetFileNameWithoutExtension(filePath);
var ext = Path.GetExtension(filePath);
var i = 1;
string candidate;
do { candidate = Path.Combine(dir, $"{name}_{i++}{ext}"); }
while (File.Exists(candidate));
return candidate;
}
private void LogExtractionSummary(string sourceFileName, PdfExtractionResult result)
{
logger.LogInformation(
"PDF '{FileName}': {Total} Anhang/Anhänge extrahiert. ZUGFeRD-XML: {HasXml}",
sourceFileName,
result.Attachments.Count,
result.HasZugferdXml
? $"Ja → {result.ZugferdXmlAttachment!.OriginalFileName}"
: "Nein");
}
}

View File

@@ -0,0 +1,35 @@
namespace DXApp.TemplateKitProject.Services
{
using DevExpress.Pdf;
using System.Text;
public class ZugferdExtractorService
{
private static readonly string[] KnownFileNames =
[
"zugferd-invoice.xml",
"factur-x.xml",
"xrechnung.xml"
];
public string? ExtractXml(Stream pdfStream)
{
using var processor = new PdfDocumentProcessor();
processor.LoadDocument(pdfStream);
foreach (var attachment in processor.Document.FileAttachments)
{
bool isZugferd = KnownFileNames.Any(name =>
attachment.FileName.Equals(name, StringComparison.OrdinalIgnoreCase));
if (isZugferd || attachment.MimeType == "text/xml")
{
byte[] data = attachment.Data;
return Encoding.UTF8.GetString(data);
}
}
return null; // Kein ZUGFeRD-Anhang gefunden
}
}
}

View File

@@ -0,0 +1,30 @@
using DXApp.TemplateKitProject.Data;
using DXApp.TemplateKitProject.Models;
namespace DXApp.TemplateKitProject.Services;
public class ZugferdImportService(
ZugferdExtractorService extractor,
ZugferdParserService parser,
AppDbContext db,
ILogger<ZugferdImportService> logger)
{
public async Task<ZugferdInvoice?> ImportAsync(Stream pdfStream, string sourceType)
{
var xml = extractor.ExtractXml(pdfStream);
if (xml is null)
{
logger.LogWarning("Kein ZUGFeRD-XML in der PDF-Datei gefunden.");
return null;
}
var invoice = parser.Parse(xml);
invoice.SourceType = sourceType;
db.ZugferdInvoices.Add(invoice);
await db.SaveChangesAsync();
return invoice;
}
}

View File

@@ -0,0 +1,64 @@
namespace DXApp.TemplateKitProject.Services;
using DXApp.TemplateKitProject.Models;
using System.Xml.Linq;
public class ZugferdParserService
{
// ZUGFeRD v2 / Factur-X Namespaces
private static readonly XNamespace Ram = "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100";
private static readonly XNamespace Rsm = "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100";
private static readonly XNamespace Udt = "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100";
public ZugferdInvoice Parse(string xml)
{
var doc = XDocument.Parse(xml);
var root = doc.Root!;
var header = root.Element(Rsm + "ExchangedDocument");
var trade = root.Element(Rsm + "SupplyChainTradeTransaction");
var agreement = trade?.Element(Ram + "ApplicableHeaderTradeAgreement");
var settlement = trade?.Element(Ram + "ApplicableHeaderTradeSettlement");
return new ZugferdInvoice
{
InvoiceNumber = header?.Element(Ram + "ID")?.Value ?? string.Empty,
InvoiceDate = ParseDate(header?.Element(Ram + "IssueDateTime")
?.Element(Udt + "DateTimeString")?.Value),
SellerName = agreement?.Element(Ram + "SellerTradeParty")
?.Element(Ram + "Name")?.Value ?? string.Empty,
SellerTaxId = agreement?.Element(Ram + "SellerTradeParty")
?.Element(Ram + "SpecifiedTaxRegistration")
?.Element(Ram + "ID")?.Value ?? string.Empty,
BuyerName = agreement?.Element(Ram + "BuyerTradeParty")
?.Element(Ram + "Name")?.Value ?? string.Empty,
CurrencyCode = settlement?.Element(Ram + "InvoiceCurrencyCode")?.Value ?? "EUR",
TotalAmount = ParseDecimal(settlement?.Element(Ram + "SpecifiedTradeSettlementHeaderMonetarySummation")
?.Element(Ram + "GrandTotalAmount")?.Value),
TaxAmount = ParseDecimal(settlement?.Element(Ram + "SpecifiedTradeSettlementHeaderMonetarySummation")
?.Element(Ram + "TaxTotalAmount")?.Value),
Iban = settlement?.Element(Ram + "SpecifiedTradeSettlementPaymentMeans")
?.Element(Ram + "PayeePartyCreditorFinancialAccount")
?.Element(Ram + "IBANID")?.Value ?? string.Empty,
RawXml = xml,
ImportedAt = DateTime.UtcNow
};
}
private static DateTime ParseDate(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return DateTime.MinValue;
return DateTime.TryParseExact(value, "yyyyMMdd",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out var dt) ? dt : DateTime.MinValue;
}
private static decimal ParseDecimal(string? value)
=> decimal.TryParse(value,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture, out var d) ? d : 0m;
}