From 2075bcb2c328cfd82135274cbf74f5f9463828d2 Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Mon, 7 Sep 2026 15:34:58 +0200 Subject: [PATCH] Add local blob-triggered CSV validation with Azurite --- ImportAssist.slnx | 3 + src/ImportAssist.Cli/Program.cs | 34 ++-------- src/ImportAssist.Core/InvoiceCsvParser.cs | 41 ++++++++++++ .../ImportAssist_Functions.csproj | 1 + .../InvoiceBlobTrigger.cs | 66 +++++++++++++++++++ .../ImportAssist.Upload.csproj | 14 ++++ tools/ImportAssist.Upload/Program.cs | 21 ++++++ 7 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 src/ImportAssist.Core/InvoiceCsvParser.cs create mode 100644 src/ImportAssist.Functions/InvoiceBlobTrigger.cs create mode 100644 tools/ImportAssist.Upload/ImportAssist.Upload.csproj create mode 100644 tools/ImportAssist.Upload/Program.cs diff --git a/ImportAssist.slnx b/ImportAssist.slnx index 559b508..f65393d 100644 --- a/ImportAssist.slnx +++ b/ImportAssist.slnx @@ -6,4 +6,7 @@ + + + diff --git a/src/ImportAssist.Cli/Program.cs b/src/ImportAssist.Cli/Program.cs index bd3d151..4672e24 100644 --- a/src/ImportAssist.Cli/Program.cs +++ b/src/ImportAssist.Cli/Program.cs @@ -17,39 +17,19 @@ if (!File.Exists(path)) return; } -var lines = File.ReadAllLines(path); +IReadOnlyList rows; -if (lines.Length == 0 || - lines[0] != "InvoiceId;CustomerId;Amount;Currency") +try { - Console.Error.WriteLine( - "Die Datei ist leer oder die Kopfzeile stimmt nicht."); + rows = InvoiceCsvParser.Parse(File.ReadAllText(path)); +} +catch (FormatException exception) +{ + Console.Error.WriteLine(exception.Message); Environment.ExitCode = 1; return; } -var rows = new List(); - -for (var index = 1; index < lines.Length; index++) -{ - var line = lines[index]; - var columns = line.Split(';'); - - if (line.Contains('"') || columns.Length != 4) - { - Console.Error.WriteLine( - $"Zeile {index + 1}: Erwartet werden vier Spalten ohne Anführungszeichen."); - Environment.ExitCode = 1; - return; - } - - rows.Add(new InvoiceRow( - columns[0], - columns[1], - columns[2], - columns[3])); -} - foreach (var row in rows) { var errors = InvoiceValidator.Validate(row); diff --git a/src/ImportAssist.Core/InvoiceCsvParser.cs b/src/ImportAssist.Core/InvoiceCsvParser.cs new file mode 100644 index 0000000..cfecbbf --- /dev/null +++ b/src/ImportAssist.Core/InvoiceCsvParser.cs @@ -0,0 +1,41 @@ +namespace ImportAssist.Core; + +public static class InvoiceCsvParser +{ + public static IReadOnlyList Parse(string csv) + { + using var reader = new StringReader(csv); + + var header = reader.ReadLine()?.TrimStart('\uFEFF'); + + if (header != "InvoiceId;CustomerId;Amount;Currency") + { + throw new FormatException( + "Die Datei ist leer oder die Kopfzeile stimmt nicht."); + } + + var rows = new List(); + var lineNumber = 1; + + while (reader.ReadLine() is { } line) + { + lineNumber++; + + var columns = line.Split(';'); + + if (line.Contains('"') || columns.Length != 4) + { + throw new FormatException( + $"Zeile {lineNumber}: Erwartet werden vier Spalten ohne Anführungszeichen."); + } + + rows.Add(new InvoiceRow( + columns[0], + columns[1], + columns[2], + columns[3])); + } + + return rows; + } +} diff --git a/src/ImportAssist.Functions/ImportAssist_Functions.csproj b/src/ImportAssist.Functions/ImportAssist_Functions.csproj index f3b163d..7a4f534 100644 --- a/src/ImportAssist.Functions/ImportAssist_Functions.csproj +++ b/src/ImportAssist.Functions/ImportAssist_Functions.csproj @@ -11,6 +11,7 @@ + diff --git a/src/ImportAssist.Functions/InvoiceBlobTrigger.cs b/src/ImportAssist.Functions/InvoiceBlobTrigger.cs new file mode 100644 index 0000000..24c622a --- /dev/null +++ b/src/ImportAssist.Functions/InvoiceBlobTrigger.cs @@ -0,0 +1,66 @@ +using ImportAssist.Core; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.Logging; + +namespace ImportAssist_Functions; + +public sealed class InvoiceBlobTrigger( + ILogger logger) +{ + [Function("InvoiceBlobTrigger")] + public void Run( + [BlobTrigger( + "imports/{name}", + Connection = "AzureWebJobsStorage")] + string content, + string name) + { + IReadOnlyList rows; + + try + { + rows = InvoiceCsvParser.Parse(content); + } + catch (FormatException exception) + { + logger.LogWarning( + "Datei {BlobName} wegen CSV-Formatfehler abgewiesen: {Reason}", + name, + exception.Message); + + return; + } + + var validCount = 0; + + foreach (var row in rows) + { + var errors = InvoiceValidator.Validate(row); + + if (errors.Count == 0) + { + validCount++; + } + + var invoiceId = string.IsNullOrWhiteSpace(row.InvoiceId) + ? "(ohne Rechnungsnummer)" + : row.InvoiceId; + + logger.LogInformation( + "{BlobName}: {InvoiceId}: {Result}", + name, + invoiceId, + errors.Count == 0 + ? "Gültig" + : string.Join("; ", errors)); + } + + logger.LogInformation( + "Dateiprüfung abgeschlossen: {BlobName}, " + + "{TotalCount} Rechnungen, {ValidCount} gültig, {InvalidCount} fehlerhaft.", + name, + rows.Count, + validCount, + rows.Count - validCount); + } +} \ No newline at end of file diff --git a/tools/ImportAssist.Upload/ImportAssist.Upload.csproj b/tools/ImportAssist.Upload/ImportAssist.Upload.csproj new file mode 100644 index 0000000..54b8ca8 --- /dev/null +++ b/tools/ImportAssist.Upload/ImportAssist.Upload.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + diff --git a/tools/ImportAssist.Upload/Program.cs b/tools/ImportAssist.Upload/Program.cs new file mode 100644 index 0000000..6675362 --- /dev/null +++ b/tools/ImportAssist.Upload/Program.cs @@ -0,0 +1,21 @@ +using Azure.Storage.Blobs; + +if (args.Length != 1 || !File.Exists(args[0])) +{ + Console.Error.WriteLine("Bitte eine vorhandene Datei angeben."); + Environment.ExitCode = 1; + return; +} + +var container = new BlobContainerClient( + "UseDevelopmentStorage=true", + "imports"); + +await container.CreateIfNotExistsAsync(); + +var blobName = $"{Guid.NewGuid():N}-{Path.GetFileName(args[0])}"; +var blob = container.GetBlobClient(blobName); + +await blob.UploadAsync(args[0]); + +Console.WriteLine($"Hochgeladen: imports/{blobName}"); \ No newline at end of file