diff --git a/src/ImportAssist.Cli/Program.cs b/src/ImportAssist.Cli/Program.cs
index 4672e24..22a439d 100644
--- a/src/ImportAssist.Cli/Program.cs
+++ b/src/ImportAssist.Cli/Program.cs
@@ -1,5 +1,6 @@
using ImportAssist.Core;
+// Einstiegspunkt für die lokale Prüfung: args[0] enthält den Pfad zur CSV-Datei.
if (args.Length != 1)
{
Console.Error.WriteLine(
@@ -25,11 +26,13 @@ try
}
catch (FormatException exception)
{
+ // Ein fehlerhafter Dateiaufbau verhindert die Prüfung aller enthaltenen Rechnungen.
Console.Error.WriteLine(exception.Message);
Environment.ExitCode = 1;
return;
}
+// Fachliche Fehler werden je Rechnung ausgegeben; sie ändern den Prozess-Exitcode nicht.
foreach (var row in rows)
{
var errors = InvoiceValidator.Validate(row);
diff --git a/src/ImportAssist.Core/ImportReport.cs b/src/ImportAssist.Core/ImportReport.cs
index a57fed2..3bfb512 100644
--- a/src/ImportAssist.Core/ImportReport.cs
+++ b/src/ImportAssist.Core/ImportReport.cs
@@ -1,11 +1,29 @@
namespace ImportAssist.Core;
+///
+/// Ordnet das fachliche Prüfergebnis einer Rechnung ihrer ursprünglichen CSV-Zeile zu.
+///
+/// Zeilennummer in der CSV-Datei; die erste Datenzeile hat die Nummer 2.
+/// Unverändert übernommene Rechnungsnummer, gegebenenfalls leer.
+/// Gibt an, ob die fachliche Prüfung keine Fehler gefunden hat.
+/// Fachliche Fehlermeldungen für diese Rechnung; bei gültigen Rechnungen leer.
public sealed record InvoiceCheckResult(
int LineNumber,
string InvoiceId,
bool IsValid,
IReadOnlyList Errors);
+///
+/// Fasst die Prüfung eines CSV-Blobs mit Ergebnissen je Rechnung oder einem Dateiformatfehler zusammen.
+///
+/// Name des geprüften Blobs im Importcontainer.
+/// Zeitpunkt der Berichtserstellung in UTC.
+///
+/// Completed bei abgeschlossener Prüfung, auch mit ungültigen Rechnungen;
+/// Rejected bei einem nicht verarbeitbaren CSV-Format.
+///
+/// Ergebnisse je Rechnung; bei einem Dateiformatfehler leer.
+/// Fehler des CSV-Dateiformats, getrennt von fachlichen Rechnungsfehlern.
public sealed record ImportReport(
string SourceBlob,
DateTimeOffset CheckedAtUtc,
@@ -13,7 +31,10 @@ public sealed record ImportReport(
IReadOnlyList Rows,
IReadOnlyList FileErrors)
{
+ /// Gesamtzahl der im Bericht enthaltenen Rechnungsprüfungen.
public int TotalCount => Rows.Count;
+ /// Anzahl der Rechnungen ohne fachliche Fehler.
public int ValidCount => Rows.Count(row => row.IsValid);
+ /// Anzahl der Rechnungen mit fachlichen Fehlern; Dateiformatfehler zählen nicht mit.
public int InvalidCount => TotalCount - ValidCount;
}
diff --git a/src/ImportAssist.Core/InvoiceCsvParser.cs b/src/ImportAssist.Core/InvoiceCsvParser.cs
index cfecbbf..800fce8 100644
--- a/src/ImportAssist.Core/InvoiceCsvParser.cs
+++ b/src/ImportAssist.Core/InvoiceCsvParser.cs
@@ -1,11 +1,25 @@
namespace ImportAssist.Core;
+///
+/// Liest Rechnungen aus dem vereinfachten, mit Semikolon getrennten Importformat.
+///
public static class InvoiceCsvParser
{
+ ///
+ /// Prüft den Aufbau der CSV-Datei und übernimmt die vier Spalten jeder Datenzeile als Text.
+ /// Die fachliche Prüfung der Rechnungen erfolgt separat durch .
+ ///
+ /// Vollständiger Dateiinhalt mit der Kopfzeile InvoiceId;CustomerId;Amount;Currency.
+ /// Rechnungszeilen in Dateireihenfolge; bei einer Datei nur mit Kopfzeile eine leere Liste.
+ ///
+ /// Die Kopfzeile fehlt oder weicht ab, oder eine Datenzeile enthält Anführungszeichen
+ /// beziehungsweise nicht genau vier Spalten.
+ ///
public static IReadOnlyList Parse(string csv)
{
using var reader = new StringReader(csv);
+ // Ein optionales Unicode-BOM am Dateianfang gehört nicht zum Namen der ersten Spalte.
var header = reader.ReadLine()?.TrimStart('\uFEFF');
if (header != "InvoiceId;CustomerId;Amount;Currency")
@@ -21,6 +35,8 @@ public static class InvoiceCsvParser
{
lineNumber++;
+ // Das Importformat unterstützt keine in Anführungszeichen eingeschlossenen Felder.
+ // Auch Leerzeilen werden als Datenzeilen geprüft und nicht übersprungen.
var columns = line.Split(';');
if (line.Contains('"') || columns.Length != 4)
diff --git a/src/ImportAssist.Core/InvoiceRow.cs b/src/ImportAssist.Core/InvoiceRow.cs
index 63914c5..0662b2b 100644
--- a/src/ImportAssist.Core/InvoiceRow.cs
+++ b/src/ImportAssist.Core/InvoiceRow.cs
@@ -1,5 +1,13 @@
namespace ImportAssist.Core;
+///
+/// Enthält die ungeprüften Rechnungsfelder. Der Betrag bleibt zunächst Text,
+/// damit auch ungültige Zahlen als fachlicher Prüfungsfehler gemeldet werden können.
+///
+/// Rechnungsnummer; darf bei der fachlichen Prüfung nicht leer sein.
+/// Kundennummer; darf bei der fachlichen Prüfung nicht leer sein.
+/// Betrag als Text, für die Prüfung mit Dezimalpunkt und ohne Tausendertrennzeichen.
+/// Übernommene Währungsangabe; wird derzeit nicht validiert.
public sealed record InvoiceRow(
string InvoiceId,
string CustomerId,
diff --git a/src/ImportAssist.Core/InvoiceValidator.cs b/src/ImportAssist.Core/InvoiceValidator.cs
index c6bbfb7..ca62983 100644
--- a/src/ImportAssist.Core/InvoiceValidator.cs
+++ b/src/ImportAssist.Core/InvoiceValidator.cs
@@ -2,8 +2,17 @@ using System.Globalization;
namespace ImportAssist.Core;
+///
+/// Bündelt die fachlichen Rechnungsprüfungen für CLI, HTTP-Aufrufe und Blob-Importe.
+///
public static class InvoiceValidator
{
+ ///
+ /// Prüft Rechnungsnummer, Kundennummer und einen positiven, lesbaren Betrag.
+ /// Die Währung wird derzeit nicht geprüft.
+ ///
+ /// Zu prüfende Rechnung mit den ursprünglichen Feldwerten als Text.
+ /// Alle gefundenen Fehlermeldungen; eine leere Liste kennzeichnet eine gültige Rechnung.
public static IReadOnlyList Validate(InvoiceRow row)
{
var errors = new List();
@@ -18,6 +27,8 @@ public static class InvoiceValidator
errors.Add("Kundennummer fehlt.");
}
+ // Das Zahlenformat ist unabhängig von der Serversprache: Dezimalpunkt und Vorzeichen
+ // sind erlaubt, Tausendertrennzeichen und umgebende Leerzeichen dagegen nicht.
var allowedStyles =
NumberStyles.AllowLeadingSign |
NumberStyles.AllowDecimalPoint;
diff --git a/src/ImportAssist.Functions/InvoiceBlobTrigger.cs b/src/ImportAssist.Functions/InvoiceBlobTrigger.cs
index 3ba61d0..05b89e7 100644
--- a/src/ImportAssist.Functions/InvoiceBlobTrigger.cs
+++ b/src/ImportAssist.Functions/InvoiceBlobTrigger.cs
@@ -5,6 +5,10 @@ using Microsoft.Extensions.Logging;
namespace ImportAssist_Functions;
+///
+/// Prüft CSV-Dateien aus dem Blobcontainer imports und erstellt JSON-Berichte im Container results.
+///
+/// Vom Functions-Host bereitgestellter Logger für die Zusammenfassung der Prüfung.
public sealed class InvoiceBlobTrigger(
ILogger logger)
{
@@ -14,6 +18,12 @@ public sealed class InvoiceBlobTrigger(
WriteIndented = true
};
+ ///
+ /// Liest und validiert einen importierten CSV-Blob und liefert den serialisierten Prüfbericht.
+ ///
+ /// Vom Blob-Trigger als Text geladener Dateiinhalt.
+ /// Über den Binding-Platzhalter name ermittelter Blobname im Importcontainer.
+ /// JSON-Bericht, den das Output-Binding unter results/{name}.json speichert.
[Function("InvoiceBlobTrigger")]
[BlobOutput(
"results/{name}.json",
@@ -37,6 +47,7 @@ public sealed class InvoiceBlobTrigger(
var row = rows[index];
var errors = InvoiceValidator.Validate(row);
+ // Die Kopfzeile belegt Zeile 1; der nullbasierte Index beginnt deshalb bei CSV-Zeile 2.
results.Add(new InvoiceCheckResult(
LineNumber: index + 2,
InvoiceId: row.InvoiceId,
@@ -44,6 +55,7 @@ public sealed class InvoiceBlobTrigger(
Errors: errors));
}
+ // Completed bezeichnet die abgeschlossene Prüfung, auch wenn einzelne Rechnungen ungültig sind.
report = new ImportReport(
SourceBlob: name,
CheckedAtUtc: DateTimeOffset.UtcNow,
@@ -53,6 +65,7 @@ public sealed class InvoiceBlobTrigger(
}
catch (FormatException exception)
{
+ // Bei einem Formatfehler wird die gesamte Datei abgelehnt; es gibt keine Teilergebnisse.
report = new ImportReport(
SourceBlob: name,
CheckedAtUtc: DateTimeOffset.UtcNow,
@@ -71,6 +84,7 @@ public sealed class InvoiceBlobTrigger(
report.ValidCount,
report.InvalidCount);
+ // Das Output-Binding übernimmt das Speichern; dafür ist hier kein eigener Storage-Client nötig.
return JsonSerializer.Serialize(report, JsonOptions);
}
}
\ No newline at end of file
diff --git a/src/ImportAssist.Functions/Program.cs b/src/ImportAssist.Functions/Program.cs
index cd308f9..747dcda 100644
--- a/src/ImportAssist.Functions/Program.cs
+++ b/src/ImportAssist.Functions/Program.cs
@@ -6,10 +6,13 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenTelemetry;
+// Erstellt den Functions-Worker; args enthält die Startargumente des Prozesses.
var builder = FunctionsApplication.CreateBuilder(args);
+// Aktiviert die ASP.NET-Core-Anbindung für HTTP-Trigger mit HttpRequest und IActionResult.
builder.ConfigureFunctionsWebApplication();
+// Telemetrie wird nur bei konfigurierter Application-Insights-Verbindung nach Azure exportiert.
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING")))
{
builder.Services.AddOpenTelemetry()
diff --git a/src/ImportAssist.Functions/ValidateInvoice.cs b/src/ImportAssist.Functions/ValidateInvoice.cs
index 5353d8e..7c2bf98 100644
--- a/src/ImportAssist.Functions/ValidateInvoice.cs
+++ b/src/ImportAssist.Functions/ValidateInvoice.cs
@@ -7,11 +7,23 @@ using Microsoft.Extensions.Logging;
namespace ImportAssist_Functions;
+///
+/// Stellt die fachliche Prüfung einer einzelnen Rechnung über einen HTTP-POST-Endpunkt bereit.
+///
+/// Vom Functions-Host bereitgestellter Logger für das Ergebnis der Prüfung.
public sealed class ValidateInvoice(ILogger logger)
{
private static readonly JsonSerializerOptions JsonOptions =
new(JsonSerializerDefaults.Web);
+ ///
+ /// Liest eine Rechnung aus dem JSON-Request und prüft sie mit den gemeinsamen fachlichen Regeln.
+ ///
+ /// HTTP-Anfrage mit einem Rechnungsobjekt aus Textfeldern im Request-Body.
+ ///
+ /// HTTP 400 bei ungültigem JSON beziehungsweise einem nicht lesbaren oder fehlenden Rechnungsobjekt;
+ /// andernfalls HTTP 200 mit Rechnungsnummer, Gültigkeitskennzeichen und fachlichen Fehlermeldungen.
+ ///
[Function("ValidateInvoice")]
public async Task Run(
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest request)
@@ -20,6 +32,7 @@ public sealed class ValidateInvoice(ILogger logger)
try
{
+ // Ein abgebrochener HTTP-Aufruf soll auch das Einlesen des Request-Bodys abbrechen.
row = await JsonSerializer.DeserializeAsync(
request.Body,
JsonOptions,
@@ -33,6 +46,7 @@ public sealed class ValidateInvoice(ILogger logger)
});
}
+ // Der JSON-Wert null lässt sich deserialisieren, enthält aber keine prüfbare Rechnung.
if (row is null)
{
return new BadRequestObjectResult(new
@@ -47,6 +61,7 @@ public sealed class ValidateInvoice(ILogger logger)
"Rechnungsprüfung abgeschlossen: {ErrorCount} Fehler.",
errors.Count);
+ // Fachliche Fehler gehören zum regulären Prüfergebnis und werden deshalb mit HTTP 200 geliefert.
return new OkObjectResult(new
{
invoiceId = row.InvoiceId,
diff --git a/tools/ImportAssist.Upload/Program.cs b/tools/ImportAssist.Upload/Program.cs
index 503b3c5..5ddded4 100644
--- a/tools/ImportAssist.Upload/Program.cs
+++ b/tools/ImportAssist.Upload/Program.cs
@@ -1,12 +1,16 @@
using Azure.Storage.Blobs;
+// Lokales Hilfsprogramm für Azurite (UseDevelopmentStorage=true):
+// lädt einen Import hoch; result liest dessen JSON-Prüfbericht.
if (args.Length == 2 && args[0] == "result")
{
+ // args[1] ist der beim Upload ausgegebene Blobname einschließlich der ursprünglichen Dateiendung.
var resultBlob = new BlobClient(
"UseDevelopmentStorage=true",
"results",
$"{args[1]}.json");
+ // Die Function verarbeitet Uploads asynchron; der Bericht kann daher noch fehlen.
if (!(await resultBlob.ExistsAsync()).Value)
{
Console.Error.WriteLine(
@@ -31,6 +35,7 @@ var container = new BlobContainerClient(
"UseDevelopmentStorage=true",
"imports");
+// Beide Container werden auch bei einer frisch gestarteten lokalen Storage-Umgebung angelegt.
await container.CreateIfNotExistsAsync();
var resultsContainer = new BlobContainerClient(
@@ -39,6 +44,7 @@ var resultsContainer = new BlobContainerClient(
await resultsContainer.CreateIfNotExistsAsync();
+// Die zufällige Kennung verhindert Namenskollisionen bei wiederholtem Upload derselben Datei.
var blobName = $"{Guid.NewGuid():N}-{Path.GetFileName(args[0])}";
var blob = container.GetBlobClient(blobName);