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;
|
using ImportAssist.Core;
|
||||||
|
|
||||||
|
// Einstiegspunkt für die lokale Prüfung: args[0] enthält den Pfad zur CSV-Datei.
|
||||||
if (args.Length != 1)
|
if (args.Length != 1)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -25,11 +26,13 @@ try
|
|||||||
}
|
}
|
||||||
catch (FormatException exception)
|
catch (FormatException exception)
|
||||||
{
|
{
|
||||||
|
// Ein fehlerhafter Dateiaufbau verhindert die Prüfung aller enthaltenen Rechnungen.
|
||||||
Console.Error.WriteLine(exception.Message);
|
Console.Error.WriteLine(exception.Message);
|
||||||
Environment.ExitCode = 1;
|
Environment.ExitCode = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fachliche Fehler werden je Rechnung ausgegeben; sie ändern den Prozess-Exitcode nicht.
|
||||||
foreach (var row in rows)
|
foreach (var row in rows)
|
||||||
{
|
{
|
||||||
var errors = InvoiceValidator.Validate(row);
|
var errors = InvoiceValidator.Validate(row);
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
namespace ImportAssist.Core;
|
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(
|
public sealed record InvoiceCheckResult(
|
||||||
int LineNumber,
|
int LineNumber,
|
||||||
string InvoiceId,
|
string InvoiceId,
|
||||||
bool IsValid,
|
bool IsValid,
|
||||||
IReadOnlyList<string> Errors);
|
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(
|
public sealed record ImportReport(
|
||||||
string SourceBlob,
|
string SourceBlob,
|
||||||
DateTimeOffset CheckedAtUtc,
|
DateTimeOffset CheckedAtUtc,
|
||||||
@@ -13,7 +31,10 @@ public sealed record ImportReport(
|
|||||||
IReadOnlyList<InvoiceCheckResult> Rows,
|
IReadOnlyList<InvoiceCheckResult> Rows,
|
||||||
IReadOnlyList<string> FileErrors)
|
IReadOnlyList<string> FileErrors)
|
||||||
{
|
{
|
||||||
|
/// <summary>Gesamtzahl der im Bericht enthaltenen Rechnungsprüfungen.</summary>
|
||||||
public int TotalCount => Rows.Count;
|
public int TotalCount => Rows.Count;
|
||||||
|
/// <summary>Anzahl der Rechnungen ohne fachliche Fehler.</summary>
|
||||||
public int ValidCount => Rows.Count(row => row.IsValid);
|
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;
|
public int InvalidCount => TotalCount - ValidCount;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,25 @@
|
|||||||
namespace ImportAssist.Core;
|
namespace ImportAssist.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Liest Rechnungen aus dem vereinfachten, mit Semikolon getrennten Importformat.
|
||||||
|
/// </summary>
|
||||||
public static class InvoiceCsvParser
|
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)
|
public static IReadOnlyList<InvoiceRow> Parse(string csv)
|
||||||
{
|
{
|
||||||
using var reader = new StringReader(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');
|
var header = reader.ReadLine()?.TrimStart('\uFEFF');
|
||||||
|
|
||||||
if (header != "InvoiceId;CustomerId;Amount;Currency")
|
if (header != "InvoiceId;CustomerId;Amount;Currency")
|
||||||
@@ -21,6 +35,8 @@ public static class InvoiceCsvParser
|
|||||||
{
|
{
|
||||||
lineNumber++;
|
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(';');
|
var columns = line.Split(';');
|
||||||
|
|
||||||
if (line.Contains('"') || columns.Length != 4)
|
if (line.Contains('"') || columns.Length != 4)
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
namespace ImportAssist.Core;
|
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(
|
public sealed record InvoiceRow(
|
||||||
string InvoiceId,
|
string InvoiceId,
|
||||||
string CustomerId,
|
string CustomerId,
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ using System.Globalization;
|
|||||||
|
|
||||||
namespace ImportAssist.Core;
|
namespace ImportAssist.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bündelt die fachlichen Rechnungsprüfungen für CLI, HTTP-Aufrufe und Blob-Importe.
|
||||||
|
/// </summary>
|
||||||
public static class InvoiceValidator
|
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)
|
public static IReadOnlyList<string> Validate(InvoiceRow row)
|
||||||
{
|
{
|
||||||
var errors = new List<string>();
|
var errors = new List<string>();
|
||||||
@@ -18,6 +27,8 @@ public static class InvoiceValidator
|
|||||||
errors.Add("Kundennummer fehlt.");
|
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 =
|
var allowedStyles =
|
||||||
NumberStyles.AllowLeadingSign |
|
NumberStyles.AllowLeadingSign |
|
||||||
NumberStyles.AllowDecimalPoint;
|
NumberStyles.AllowDecimalPoint;
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace ImportAssist_Functions;
|
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(
|
public sealed class InvoiceBlobTrigger(
|
||||||
ILogger<InvoiceBlobTrigger> logger)
|
ILogger<InvoiceBlobTrigger> logger)
|
||||||
{
|
{
|
||||||
@@ -14,6 +18,12 @@ public sealed class InvoiceBlobTrigger(
|
|||||||
WriteIndented = true
|
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")]
|
[Function("InvoiceBlobTrigger")]
|
||||||
[BlobOutput(
|
[BlobOutput(
|
||||||
"results/{name}.json",
|
"results/{name}.json",
|
||||||
@@ -37,6 +47,7 @@ public sealed class InvoiceBlobTrigger(
|
|||||||
var row = rows[index];
|
var row = rows[index];
|
||||||
var errors = InvoiceValidator.Validate(row);
|
var errors = InvoiceValidator.Validate(row);
|
||||||
|
|
||||||
|
// Die Kopfzeile belegt Zeile 1; der nullbasierte Index beginnt deshalb bei CSV-Zeile 2.
|
||||||
results.Add(new InvoiceCheckResult(
|
results.Add(new InvoiceCheckResult(
|
||||||
LineNumber: index + 2,
|
LineNumber: index + 2,
|
||||||
InvoiceId: row.InvoiceId,
|
InvoiceId: row.InvoiceId,
|
||||||
@@ -44,6 +55,7 @@ public sealed class InvoiceBlobTrigger(
|
|||||||
Errors: errors));
|
Errors: errors));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Completed bezeichnet die abgeschlossene Prüfung, auch wenn einzelne Rechnungen ungültig sind.
|
||||||
report = new ImportReport(
|
report = new ImportReport(
|
||||||
SourceBlob: name,
|
SourceBlob: name,
|
||||||
CheckedAtUtc: DateTimeOffset.UtcNow,
|
CheckedAtUtc: DateTimeOffset.UtcNow,
|
||||||
@@ -53,6 +65,7 @@ public sealed class InvoiceBlobTrigger(
|
|||||||
}
|
}
|
||||||
catch (FormatException exception)
|
catch (FormatException exception)
|
||||||
{
|
{
|
||||||
|
// Bei einem Formatfehler wird die gesamte Datei abgelehnt; es gibt keine Teilergebnisse.
|
||||||
report = new ImportReport(
|
report = new ImportReport(
|
||||||
SourceBlob: name,
|
SourceBlob: name,
|
||||||
CheckedAtUtc: DateTimeOffset.UtcNow,
|
CheckedAtUtc: DateTimeOffset.UtcNow,
|
||||||
@@ -71,6 +84,7 @@ public sealed class InvoiceBlobTrigger(
|
|||||||
report.ValidCount,
|
report.ValidCount,
|
||||||
report.InvalidCount);
|
report.InvalidCount);
|
||||||
|
|
||||||
|
// Das Output-Binding übernimmt das Speichern; dafür ist hier kein eigener Storage-Client nötig.
|
||||||
return JsonSerializer.Serialize(report, JsonOptions);
|
return JsonSerializer.Serialize(report, JsonOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,10 +6,13 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using OpenTelemetry;
|
using OpenTelemetry;
|
||||||
|
|
||||||
|
// Erstellt den Functions-Worker; args enthält die Startargumente des Prozesses.
|
||||||
var builder = FunctionsApplication.CreateBuilder(args);
|
var builder = FunctionsApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Aktiviert die ASP.NET-Core-Anbindung für HTTP-Trigger mit HttpRequest und IActionResult.
|
||||||
builder.ConfigureFunctionsWebApplication();
|
builder.ConfigureFunctionsWebApplication();
|
||||||
|
|
||||||
|
// Telemetrie wird nur bei konfigurierter Application-Insights-Verbindung nach Azure exportiert.
|
||||||
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING")))
|
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPLICATIONINSIGHTS_CONNECTION_STRING")))
|
||||||
{
|
{
|
||||||
builder.Services.AddOpenTelemetry()
|
builder.Services.AddOpenTelemetry()
|
||||||
|
|||||||
@@ -7,11 +7,23 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace ImportAssist_Functions;
|
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)
|
public sealed class ValidateInvoice(ILogger<ValidateInvoice> logger)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions JsonOptions =
|
private static readonly JsonSerializerOptions JsonOptions =
|
||||||
new(JsonSerializerDefaults.Web);
|
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")]
|
[Function("ValidateInvoice")]
|
||||||
public async Task<IActionResult> Run(
|
public async Task<IActionResult> Run(
|
||||||
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest request)
|
[HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest request)
|
||||||
@@ -20,6 +32,7 @@ public sealed class ValidateInvoice(ILogger<ValidateInvoice> logger)
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Ein abgebrochener HTTP-Aufruf soll auch das Einlesen des Request-Bodys abbrechen.
|
||||||
row = await JsonSerializer.DeserializeAsync<InvoiceRow>(
|
row = await JsonSerializer.DeserializeAsync<InvoiceRow>(
|
||||||
request.Body,
|
request.Body,
|
||||||
JsonOptions,
|
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)
|
if (row is null)
|
||||||
{
|
{
|
||||||
return new BadRequestObjectResult(new
|
return new BadRequestObjectResult(new
|
||||||
@@ -47,6 +61,7 @@ public sealed class ValidateInvoice(ILogger<ValidateInvoice> logger)
|
|||||||
"Rechnungsprüfung abgeschlossen: {ErrorCount} Fehler.",
|
"Rechnungsprüfung abgeschlossen: {ErrorCount} Fehler.",
|
||||||
errors.Count);
|
errors.Count);
|
||||||
|
|
||||||
|
// Fachliche Fehler gehören zum regulären Prüfergebnis und werden deshalb mit HTTP 200 geliefert.
|
||||||
return new OkObjectResult(new
|
return new OkObjectResult(new
|
||||||
{
|
{
|
||||||
invoiceId = row.InvoiceId,
|
invoiceId = row.InvoiceId,
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
using Azure.Storage.Blobs;
|
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")
|
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(
|
var resultBlob = new BlobClient(
|
||||||
"UseDevelopmentStorage=true",
|
"UseDevelopmentStorage=true",
|
||||||
"results",
|
"results",
|
||||||
$"{args[1]}.json");
|
$"{args[1]}.json");
|
||||||
|
|
||||||
|
// Die Function verarbeitet Uploads asynchron; der Bericht kann daher noch fehlen.
|
||||||
if (!(await resultBlob.ExistsAsync()).Value)
|
if (!(await resultBlob.ExistsAsync()).Value)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -31,6 +35,7 @@ var container = new BlobContainerClient(
|
|||||||
"UseDevelopmentStorage=true",
|
"UseDevelopmentStorage=true",
|
||||||
"imports");
|
"imports");
|
||||||
|
|
||||||
|
// Beide Container werden auch bei einer frisch gestarteten lokalen Storage-Umgebung angelegt.
|
||||||
await container.CreateIfNotExistsAsync();
|
await container.CreateIfNotExistsAsync();
|
||||||
|
|
||||||
var resultsContainer = new BlobContainerClient(
|
var resultsContainer = new BlobContainerClient(
|
||||||
@@ -39,6 +44,7 @@ var resultsContainer = new BlobContainerClient(
|
|||||||
|
|
||||||
await resultsContainer.CreateIfNotExistsAsync();
|
await resultsContainer.CreateIfNotExistsAsync();
|
||||||
|
|
||||||
|
// Die zufällige Kennung verhindert Namenskollisionen bei wiederholtem Upload derselben Datei.
|
||||||
var blobName = $"{Guid.NewGuid():N}-{Path.GetFileName(args[0])}";
|
var blobName = $"{Guid.NewGuid():N}-{Path.GetFileName(args[0])}";
|
||||||
var blob = container.GetBlobClient(blobName);
|
var blob = container.GetBlobClient(blobName);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user