docs: Deutsche Methoden- und Codekommentare ergänzen
This commit is contained in:
1 parent
bb9b20cc25
commit
11e7f68304
9 files changed
+97
No files matched your search
@@ -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);
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
namespace ImportAssist.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Ordnet das fachliche Prüfergebnis einer Rechnung ihrer ursprünglichen CSV-Zeile zu.
|
||||
/// </summary>
|
||||
/// <param name="LineNumber">Zeilennummer in der CSV-Datei; die erste Datenzeile hat die Nummer 2.</param>
|
||||
/// <param name="InvoiceId">Unverändert übernommene Rechnungsnummer, gegebenenfalls leer.</param>
|
||||
/// <param name="IsValid">Gibt an, ob die fachliche Prüfung keine Fehler gefunden hat.</param>
|
||||
/// <param name="Errors">Fachliche Fehlermeldungen für diese Rechnung; bei gültigen Rechnungen leer.</param>
|
||||
public sealed record InvoiceCheckResult(
|
||||
int LineNumber,
|
||||
string InvoiceId,
|
||||
bool IsValid,
|
||||
IReadOnlyList<string> Errors);
|
||||
|
||||
/// <summary>
|
||||
/// Fasst die Prüfung eines CSV-Blobs mit Ergebnissen je Rechnung oder einem Dateiformatfehler zusammen.
|
||||
/// </summary>
|
||||
/// <param name="SourceBlob">Name des geprüften Blobs im Importcontainer.</param>
|
||||
/// <param name="CheckedAtUtc">Zeitpunkt der Berichtserstellung in UTC.</param>
|
||||
/// <param name="Status">
|
||||
/// Completed bei abgeschlossener Prüfung, auch mit ungültigen Rechnungen;
|
||||
/// Rejected bei einem nicht verarbeitbaren CSV-Format.
|
||||
/// </param>
|
||||
/// <param name="Rows">Ergebnisse je Rechnung; bei einem Dateiformatfehler leer.</param>
|
||||
/// <param name="FileErrors">Fehler des CSV-Dateiformats, getrennt von fachlichen Rechnungsfehlern.</param>
|
||||
public sealed record ImportReport(
|
||||
string SourceBlob,
|
||||
DateTimeOffset CheckedAtUtc,
|
||||
@@ -13,7 +31,10 @@ public sealed record ImportReport(
|
||||
IReadOnlyList<InvoiceCheckResult> Rows,
|
||||
IReadOnlyList<string> FileErrors)
|
||||
{
|
||||
/// <summary>Gesamtzahl der im Bericht enthaltenen Rechnungsprüfungen.</summary>
|
||||
public int TotalCount => Rows.Count;
|
||||
/// <summary>Anzahl der Rechnungen ohne fachliche Fehler.</summary>
|
||||
public int ValidCount => Rows.Count(row => row.IsValid);
|
||||
/// <summary>Anzahl der Rechnungen mit fachlichen Fehlern; Dateiformatfehler zählen nicht mit.</summary>
|
||||
public int InvalidCount => TotalCount - ValidCount;
|
||||
}
|
||||
@@ -1,11 +1,25 @@
|
||||
namespace ImportAssist.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Liest Rechnungen aus dem vereinfachten, mit Semikolon getrennten Importformat.
|
||||
/// </summary>
|
||||
public static class InvoiceCsvParser
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see cref="InvoiceValidator"/>.
|
||||
/// </summary>
|
||||
/// <param name="csv">Vollständiger Dateiinhalt mit der Kopfzeile InvoiceId;CustomerId;Amount;Currency.</param>
|
||||
/// <returns>Rechnungszeilen in Dateireihenfolge; bei einer Datei nur mit Kopfzeile eine leere Liste.</returns>
|
||||
/// <exception cref="FormatException">
|
||||
/// Die Kopfzeile fehlt oder weicht ab, oder eine Datenzeile enthält Anführungszeichen
|
||||
/// beziehungsweise nicht genau vier Spalten.
|
||||
/// </exception>
|
||||
public static IReadOnlyList<InvoiceRow> 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)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
namespace ImportAssist.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="InvoiceId">Rechnungsnummer; darf bei der fachlichen Prüfung nicht leer sein.</param>
|
||||
/// <param name="CustomerId">Kundennummer; darf bei der fachlichen Prüfung nicht leer sein.</param>
|
||||
/// <param name="Amount">Betrag als Text, für die Prüfung mit Dezimalpunkt und ohne Tausendertrennzeichen.</param>
|
||||
/// <param name="Currency">Übernommene Währungsangabe; wird derzeit nicht validiert.</param>
|
||||
public sealed record InvoiceRow(
|
||||
string InvoiceId,
|
||||
string CustomerId,
|
||||
|
||||
@@ -2,8 +2,17 @@ using System.Globalization;
|
||||
|
||||
namespace ImportAssist.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Bündelt die fachlichen Rechnungsprüfungen für CLI, HTTP-Aufrufe und Blob-Importe.
|
||||
/// </summary>
|
||||
public static class InvoiceValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Prüft Rechnungsnummer, Kundennummer und einen positiven, lesbaren Betrag.
|
||||
/// Die Währung wird derzeit nicht geprüft.
|
||||
/// </summary>
|
||||
/// <param name="row">Zu prüfende Rechnung mit den ursprünglichen Feldwerten als Text.</param>
|
||||
/// <returns>Alle gefundenen Fehlermeldungen; eine leere Liste kennzeichnet eine gültige Rechnung.</returns>
|
||||
public static IReadOnlyList<string> Validate(InvoiceRow row)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
@@ -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;
|
||||
|
||||
@@ -5,6 +5,10 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ImportAssist_Functions;
|
||||
|
||||
/// <summary>
|
||||
/// Prüft CSV-Dateien aus dem Blobcontainer imports und erstellt JSON-Berichte im Container results.
|
||||
/// </summary>
|
||||
/// <param name="logger">Vom Functions-Host bereitgestellter Logger für die Zusammenfassung der Prüfung.</param>
|
||||
public sealed class InvoiceBlobTrigger(
|
||||
ILogger<InvoiceBlobTrigger> logger)
|
||||
{
|
||||
@@ -14,6 +18,12 @@ public sealed class InvoiceBlobTrigger(
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Liest und validiert einen importierten CSV-Blob und liefert den serialisierten Prüfbericht.
|
||||
/// </summary>
|
||||
/// <param name="content">Vom Blob-Trigger als Text geladener Dateiinhalt.</param>
|
||||
/// <param name="name">Über den Binding-Platzhalter name ermittelter Blobname im Importcontainer.</param>
|
||||
/// <returns>JSON-Bericht, den das Output-Binding unter results/{name}.json speichert.</returns>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -7,11 +7,23 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ImportAssist_Functions;
|
||||
|
||||
/// <summary>
|
||||
/// Stellt die fachliche Prüfung einer einzelnen Rechnung über einen HTTP-POST-Endpunkt bereit.
|
||||
/// </summary>
|
||||
/// <param name="logger">Vom Functions-Host bereitgestellter Logger für das Ergebnis der Prüfung.</param>
|
||||
public sealed class ValidateInvoice(ILogger<ValidateInvoice> logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions =
|
||||
new(JsonSerializerDefaults.Web);
|
||||
|
||||
/// <summary>
|
||||
/// Liest eine Rechnung aus dem JSON-Request und prüft sie mit den gemeinsamen fachlichen Regeln.
|
||||
/// </summary>
|
||||
/// <param name="request">HTTP-Anfrage mit einem Rechnungsobjekt aus Textfeldern im Request-Body.</param>
|
||||
/// <returns>
|
||||
/// HTTP 400 bei ungültigem JSON beziehungsweise einem nicht lesbaren oder fehlenden Rechnungsobjekt;
|
||||
/// andernfalls HTTP 200 mit Rechnungsnummer, Gültigkeitskennzeichen und fachlichen Fehlermeldungen.
|
||||
/// </returns>
|
||||
[Function("ValidateInvoice")]
|
||||
public async Task<IActionResult> Run(
|
||||
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest request)
|
||||
@@ -20,6 +32,7 @@ public sealed class ValidateInvoice(ILogger<ValidateInvoice> logger)
|
||||
|
||||
try
|
||||
{
|
||||
// Ein abgebrochener HTTP-Aufruf soll auch das Einlesen des Request-Bodys abbrechen.
|
||||
row = await JsonSerializer.DeserializeAsync<InvoiceRow>(
|
||||
request.Body,
|
||||
JsonOptions,
|
||||
@@ -33,6 +46,7 @@ public sealed class ValidateInvoice(ILogger<ValidateInvoice> 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<ValidateInvoice> 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,
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
using Azure.Storage.Blobs;
|
||||
|
||||
// Lokales Hilfsprogramm für Azurite (UseDevelopmentStorage=true):
|
||||
// <Dateipfad> lädt einen Import hoch; result <Blobname> 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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user